Skip to content

Commit

Permalink
Add mod badge #522
Browse files Browse the repository at this point in the history
  • Loading branch information
mdirolf committed May 31, 2024
1 parent 10d614a commit 96a81cb
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 9 deletions.
2 changes: 2 additions & 0 deletions app/components/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface AuthContextValue {
notifications?: NotificationT[];
isAdmin: boolean;
isPatron: boolean;
isMod: boolean;
loading: boolean;
error?: string;
constructorPage?: ConstructorPageT;
Expand All @@ -21,6 +22,7 @@ export const AuthContext = createContext<AuthContextValue>({
user: undefined,
isAdmin: false,
isPatron: false,
isMod: false,
loading: false,
error: 'using default context',
constructorPage: undefined,
Expand Down
12 changes: 11 additions & 1 deletion app/components/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import styles from './Comments.module.css';
import { DisplayNameForm, useDisplayName } from './DisplayNameForm.js';
import { Emoji } from './Emoji.js';
import { GoogleLinkButton, GoogleSignInButton } from './GoogleButtons.js';
import { PatronIcon } from './Icons.js';
import { ModIcon, PatronIcon } from './Icons.js';
import { LengthLimitedTextarea, LengthView } from './Inputs.js';
import { Link } from './Link.js';
import { Markdown } from './Markdown.js';
Expand Down Expand Up @@ -138,6 +138,7 @@ const CommentView = (props: CommentProps) => {
didCheat={props.comment.authorCheated}
downsOnly={props.comment.authorSolvedDownsOnly}
isPatron={props.comment.authorIsPatron}
isMod={props.comment.authorIsMod}
/>
</div>
<Markdown hast={props.comment.commentHast} />
Expand Down Expand Up @@ -354,6 +355,7 @@ interface CommentFlairProps {
downsOnly: boolean;
displayName: string;
isPatron: boolean;
isMod: boolean;
userId: string;
username?: string;
puzzleAuthorId: string;
Expand All @@ -365,6 +367,7 @@ const CommentFlair = (props: CommentFlairProps) => {
return (
<>
{props.isPatron ? <PatronIcon linkIt={true} /> : ''}
{props.isMod ? <ModIcon /> : ''}
<i>
{' '}
<CommentAuthor
Expand Down Expand Up @@ -411,6 +414,7 @@ const CommentFlair = (props: CommentFlairProps) => {
interface CommentFormProps {
username?: string;
isPatron: boolean;
isMod: boolean;
puzzlePublishTime: number;
puzzleAuthorId: string;
hasGuestConstructor: boolean;
Expand Down Expand Up @@ -480,6 +484,7 @@ const CommentForm = ({
authorSolvedDownsOnly: comment.do || false,
publishTime: comment.p.toMillis(),
authorIsPatron: props.isPatron,
authorIsMod: props.isMod,
replyTo: comment.rt,
});

Expand Down Expand Up @@ -563,6 +568,7 @@ const CommentForm = ({
hasGuestConstructor={props.hasGuestConstructor}
username={props.username}
isPatron={props.isPatron}
isMod={props.isMod}
displayName={displayName}
userId={props.user.uid}
puzzleAuthorId={props.puzzleAuthorId}
Expand Down Expand Up @@ -761,6 +767,7 @@ export const Comments = ({
authorSolvedDownsOnly: c.do || false,
publishTime: c.p.toMillis(),
authorIsPatron: authContext.isPatron,
authorIsMod: authContext.isMod,
replyTo: c.rt,
};
mergeComment(localComment);
Expand Down Expand Up @@ -799,6 +806,7 @@ export const Comments = ({
props.clueMap,
comments,
authContext.isPatron,
authContext.isMod,
submittedComments,
submittedDeletes,
]);
Expand Down Expand Up @@ -840,6 +848,7 @@ export const Comments = ({
username={authContext.constructorPage?.i}
user={authContext.user}
isPatron={authContext.isPatron}
isMod={authContext.isMod}
onSubmit={(newComment) => {
setSubmittedComments([...submittedComments, newComment]);
}}
Expand All @@ -853,6 +862,7 @@ export const Comments = ({
user={authContext.user}
constructorPage={authContext.constructorPage}
isPatron={authContext.isPatron}
isMod={authContext.isMod}
comment={a}
onSubmit={(newComment) => {
setSubmittedComments([...submittedComments, newComment]);
Expand Down
4 changes: 3 additions & 1 deletion app/components/ConstructorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import styles from './ConstructorPage.module.css';
import { ConstructorStats } from './ConstructorStats.js';
import { FollowButton } from './FollowButton.js';
import { I18nTags } from './I18nTags.js';
import { PatronIcon } from './Icons.js';
import { ModIcon, PatronIcon } from './Icons.js';
import { CoverPic, ProfilePicAndName } from './Images.js';
import { Link, LinkButtonSimpleA } from './Link.js';
import { Markdown } from './Markdown.js';
Expand Down Expand Up @@ -196,6 +196,7 @@ export interface ConstructorPageProps {
constructorData: ConstructorPageT;
bioHast: Root;
isPatron: boolean;
isMod: boolean;
followCount: number;
followers: (ConstructorPageBase & { isPatron: boolean })[];
following: (ConstructorPageBase & { isPatron: boolean })[];
Expand Down Expand Up @@ -315,6 +316,7 @@ export const ConstructorPage = (props: ConstructorPageProps) => {
) : (
''
)}
{props.isMod ? <ModIcon className={styles.patronicon} /> : ''}
<span>{props.constructorData.n}</span>
</>
}
Expand Down
11 changes: 11 additions & 0 deletions app/components/Icons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FaCheck, FaEye } from 'react-icons/fa';
import { FaShieldHeart } from 'react-icons/fa6';
import { CheatUnit, PrefillSquares, Symmetry } from '../lib/types.js';
import { clsx } from '../lib/utils.js';
import { Link } from './Link.js';
Expand Down Expand Up @@ -497,6 +498,16 @@ const BlankLogo = () => {
);
};

export const ModIcon = (props: { className?: string }) => {
return (
<Link title="Crosshare Moderator" href="/articles/moderation">
<FaShieldHeart
className={clsx(props.className, 'verticalAlignTextTop', 'colorText')}
/>
</Link>
);
};

export const PatronIcon = (props: { className?: string; linkIt?: boolean }) => {
const icon = (
<svg
Expand Down
31 changes: 30 additions & 1 deletion app/lib/patron.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getAuth } from 'firebase-admin/auth';
import { ConstructorPageV } from './constructorPage.js';
import { DonationsListV, donationsByEmail } from './dbtypes.js';
import { AdminSettingsV, DonationsListV, donationsByEmail } from './dbtypes.js';
import { getAdminApp, getCollection } from './firebaseAdminWrapper.js';
import { PathReporter } from './pathReporter.js';

Expand Down Expand Up @@ -72,3 +72,32 @@ export const isUserPatron = async (userId: string) => {
}
return patronList.includes(userId) || false;
};

let modsList: string[] | null = null;
let modsLastUpdated: number | null = null;

async function getModsListOnce() {
const res = await getCollection('settings').doc('settings').get();
const val = AdminSettingsV.decode(res.data());
if (val._tag === 'Right') {
modsLastUpdated = Date.now();
return val.right.crypticMods ?? [];
} else {
const error = `Malformed settings doc`;
console.log(error);
console.error(PathReporter.report(val).join(','));
throw new Error(error);
}
}

export const isUserMod = async (userId: string) => {
if (
modsList === null ||
!modsLastUpdated ||
Date.now() - modsLastUpdated > 60 * 60 * 1000
) {
modsList = await getModsListOnce();
}

return modsList.includes(userId) || false;
};
4 changes: 3 additions & 1 deletion app/lib/serverOnly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from './embedOptions.js';
import { markdownToHast } from './markdown/markdown.js';
import { PathReporter } from './pathReporter.js';
import { isUserPatron } from './patron.js';
import { isUserMod, isUserPatron } from './patron.js';
import {
Comment,
Direction,
Expand Down Expand Up @@ -216,6 +216,7 @@ async function convertComments(
replies: await convertComments(c.r || [], clueMap),
...(c.un && { authorUsername: c.un }),
authorIsPatron: await isUserPatron(c.a),
authorIsMod: await isUserMod(c.a),
...(c.deleted && { deleted: true }),
...(c.removed && { removed: true }),
};
Expand Down Expand Up @@ -294,6 +295,7 @@ export const getPuzzlePageProps: GetServerSideProps<PuzzlePageProps> = async ({
: null,
constructorPage: await userIdToPage(validationResult.right.a),
constructorIsPatron: await isUserPatron(validationResult.right.a),
constructorIsMod: await isUserMod(validationResult.right.a),
comments: await convertComments(fromDB.comments, clueMap),
clueHasts: grid.entries.map((c) =>
markdownToHast({ text: c.clue, clueMap, inline: true })
Expand Down
1 change: 1 addition & 0 deletions app/lib/testingUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const WithAllProviders: (
value={{
user: undefined,
isAdmin: false,
isMod: false,
isPatron: false,
loading: false,
error: undefined,
Expand Down
2 changes: 2 additions & 0 deletions app/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export interface Comment {
id: string;
replies?: Comment[];
authorIsPatron: boolean;
authorIsMod: boolean;
/** has the comment been deleted/removed (might still be in thread if there were replies) */
deleted?: boolean;
removed?: boolean;
Expand Down Expand Up @@ -226,6 +227,7 @@ export interface ServerPuzzleResult
constructorNotes: Root | null;
constructorPage: ConstructorPageWithMarkdown | null;
constructorIsPatron: boolean;
constructorIsMod: boolean;
clueHasts: Root[];
likes: Record<string, (ConstructorPageBase & { isPatron: boolean }) | null>;
}
Expand Down
6 changes: 5 additions & 1 deletion app/lib/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function useAuth(): AuthContextValue {
);

const [isPatron, setIsPatron] = useState(false);
const [isMod, setIsMod] = useState(false);
useEffect(() => {
if (!user) {
return;
Expand All @@ -170,7 +171,9 @@ export function useAuth(): AuthContextValue {
});
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!didCancel && res) {
setIsPatron(parseUserInfo(res).isPatron);
const ui = parseUserInfo(res);
setIsPatron(ui.isPatron);
setIsMod(ui.isPatron);
}
}
logAsyncErrors(getUserInfo)();
Expand All @@ -182,6 +185,7 @@ export function useAuth(): AuthContextValue {
return {
user: user || undefined,
isPatron,
isMod,
isAdmin,
constructorPage,
notifications,
Expand Down
1 change: 1 addition & 0 deletions app/lib/userinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { PathReporter } from './pathReporter.js';

const UserInfoV = t.type({
isPatron: t.boolean,
isMod: t.boolean,
});
export type UserInfoT = t.TypeOf<typeof UserInfoV>;

Expand Down
4 changes: 3 additions & 1 deletion app/pages/[...slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getCollection } from '../lib/firebaseAdminWrapper.js';
import { markdownToHast } from '../lib/markdown/markdown.js';
import { paginatedPuzzles } from '../lib/paginatedPuzzles.js';
import { PathReporter } from '../lib/pathReporter.js';
import { isUserPatron } from '../lib/patron.js';
import { isUserMod, isUserPatron } from '../lib/patron.js';
import { AccountPrefsV } from '../lib/prefs.js';
import {
getStorageUrl,
Expand Down Expand Up @@ -104,6 +104,7 @@ const gssp: GetServerSideProps<PageProps> = async ({ res, params }) => {
const [puzzles, hasNext] = await paginatedPuzzles(page, PAGE_SIZE, 'a', cp.u);

const isPatron = await isUserPatron(cp.u);
const isMod = await isUserMod(cp.u);

const followerIds = await getFollowerIds(cp.u);
const followers = (
Expand All @@ -124,6 +125,7 @@ const gssp: GetServerSideProps<PageProps> = async ({ res, params }) => {
constructorData: cp,
bioHast: markdownToHast({ text: cp.b }),
isPatron,
isMod,
profilePicture,
coverPicture,
puzzles,
Expand Down
1 change: 1 addition & 0 deletions app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default function CrosshareApp({
loading: true,
isAdmin: false,
isPatron: false,
isMod: false,
notifications: [],
};
}
Expand Down
2 changes: 1 addition & 1 deletion app/pages/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const PuzzleListItem = (props: PuzzleResult & { crypticMods: string[] }) => {
))}
</ul>
{props.likes.length > 0 ? <div>Likes: {props.likes.length}</div> : ''}
{modLikes.length > 0 ? (
{props.userTags?.includes('cryptic') && modLikes.length > 0 ? (
<div>Cryptic Mod Likes: {modLikes.length}</div>
) : (
''
Expand Down
5 changes: 3 additions & 2 deletions app/pages/api/userinfo/[userId].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { isUserPatron } from '../../../lib/patron.js';
import { isUserMod, isUserPatron } from '../../../lib/patron.js';
import { UserInfoT } from '../../../lib/userinfo.js';

export default async function userinfo(
Expand All @@ -12,7 +12,8 @@ export default async function userinfo(
return;
}
const isPatron = await isUserPatron(userId);
const ui: UserInfoT = { isPatron };
const isMod = await isUserMod(userId);
const ui: UserInfoT = { isPatron, isMod };
res.setHeader('X-Robots-Tag', 'noindex');
res.setHeader('Cache-Control', 'public, max-age=172800, s-maxage=172800');
res.writeHead(200, { 'Content-Type': 'application/json' });
Expand Down

0 comments on commit 96a81cb

Please sign in to comment.