From 6b5da8cfb121959994b454e9e41dbbb04dea698b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 1 Oct 2024 14:40:02 -0400 Subject: [PATCH] Fixing titleOnly, PostSort, and CommentSort. (#2715) * Fixing titleOnly, PostSort, and CommentSort. * SortType, Tagline, Emojis (#2718) * PostSortType * Hide post sort types in comment view * Tagline * CustomEmoji * Update lemmy-js-client to 0.20.0-alpha.17 * Prompt before leaving unsaved forms * Add cancel buttons, only create taglines when saving * Cleanup SortSelect * Use markdown url for custom emojis This prevent SSR and CSR from rendering different images after changing the image of an emoji, already posted emojis will keep showing the old image. This will also display the same image on different instances that have overlapping custom emojis. * Cleanup EmojisForm sorting * Use existing CommentSortSelect * Simpler sort type conversion --------- Co-authored-by: matc-pub <161147791+matc-pub@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/assets/css/main.css | 4 + src/client/index.tsx | 3 +- .../components/common/comment-sort-select.tsx | 10 +- src/shared/components/common/paginator.tsx | 3 + src/shared/components/common/sort-select.tsx | 48 +- .../components/community/communities.tsx | 10 +- src/shared/components/community/community.tsx | 47 +- src/shared/components/home/admin-settings.tsx | 41 +- src/shared/components/home/emojis-form.tsx | 529 +++++++++++------- src/shared/components/home/home.tsx | 41 +- src/shared/components/home/site-form.tsx | 37 -- src/shared/components/home/tagline-form.tsx | 239 ++++++-- .../components/person/person-details.tsx | 4 +- src/shared/components/person/profile.tsx | 10 +- src/shared/components/person/settings.tsx | 60 +- src/shared/components/post/post-listing.tsx | 6 +- src/shared/components/post/post.tsx | 43 +- src/shared/components/search.tsx | 48 +- src/shared/markdown.ts | 100 ++-- .../utils/app/comment-to-post-sort-type.ts | 21 + .../utils/app/convert-comment-sort-type.ts | 31 - src/shared/utils/app/index.ts | 4 +- src/shared/utils/app/initialize-site.ts | 5 +- .../utils/app/post-to-comment-sort-type.ts | 39 +- 26 files changed, 815 insertions(+), 580 deletions(-) create mode 100644 src/shared/utils/app/comment-to-post-sort-type.ts delete mode 100644 src/shared/utils/app/convert-comment-sort-type.ts diff --git a/package.json b/package.json index 753d8d36..d069e7c4 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "inferno-router": "^8.2.3", "inferno-server": "^8.2.3", "jwt-decode": "^4.0.0", - "lemmy-js-client": "0.20.0-alpha.7", + "lemmy-js-client": "0.20.0-alpha.17", "lodash.isequal": "^4.5.0", "markdown-it": "^14.1.0", "markdown-it-bidi": "^0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27404751..50791604 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,8 +114,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 lemmy-js-client: - specifier: 0.20.0-alpha.7 - version: 0.20.0-alpha.7 + specifier: 0.20.0-alpha.17 + version: 0.20.0-alpha.17 lodash.isequal: specifier: ^4.5.0 version: 4.5.0 @@ -3071,8 +3071,8 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lemmy-js-client@0.20.0-alpha.7: - resolution: {integrity: sha512-lhPs8gJFLX0EvlwkkgtTF2F/v22lKjcQ81u7u5CShrO05Dii33fZ5x9SrEJYHD0qw4FIN/jpfKucN9QnndScpA==} + lemmy-js-client@0.20.0-alpha.17: + resolution: {integrity: sha512-4iZQtZNldhioTecSgi1LMR4E3uK5IcQ+EuWg4aAXmciOIHxPXPAHy7qSLuHqbzEiL1QP5G3MFwQnlVf/sJkFaQ==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -7811,7 +7811,7 @@ snapshots: leac@0.6.0: {} - lemmy-js-client@0.20.0-alpha.7: {} + lemmy-js-client@0.20.0-alpha.17: {} leven@3.1.0: {} diff --git a/src/assets/css/main.css b/src/assets/css/main.css index a7d7e706..34666101 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -461,3 +461,7 @@ br.big { .totp-link { width: fit-content; } + +em-emoji-picker { + width: 100%; +} diff --git a/src/client/index.tsx b/src/client/index.tsx index 27031189..432b6231 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -5,6 +5,7 @@ import App from "../shared/components/app/app"; import { lazyHighlightjs } from "../shared/lazy-highlightjs"; import { loadUserLanguage } from "../shared/services/I18NextService"; import { verifyDynamicImports } from "../shared/dynamic-imports"; +import { setupEmojiDataModel } from "../shared/markdown"; import "bootstrap/js/dist/collapse"; import "bootstrap/js/dist/dropdown"; @@ -22,7 +23,7 @@ async function startClient() { lazyHighlightjs.enableLazyLoading(); - await loadUserLanguage(); + await Promise.all([loadUserLanguage(), setupEmojiDataModel()]); const wrapper = ( diff --git a/src/shared/components/common/comment-sort-select.tsx b/src/shared/components/common/comment-sort-select.tsx index f9115583..0a37dc27 100644 --- a/src/shared/components/common/comment-sort-select.tsx +++ b/src/shared/components/common/comment-sort-select.tsx @@ -47,13 +47,13 @@ export class CommentSortSelect extends Component< - , - + - , - - + + + { @@ -18,6 +19,7 @@ export class Paginator extends Component { @@ -26,6 +28,7 @@ export class Paginator extends Component { diff --git a/src/shared/components/common/sort-select.tsx b/src/shared/components/common/sort-select.tsx index a73b1b8d..64b4a219 100644 --- a/src/shared/components/common/sort-select.tsx +++ b/src/shared/components/common/sort-select.tsx @@ -1,19 +1,19 @@ import { randomStr } from "@utils/helpers"; import { Component, linkEvent } from "inferno"; -import { SortType } from "lemmy-js-client"; +import { PostSortType } from "lemmy-js-client"; import { relTags, sortingHelpUrl } from "../../config"; import { I18NextService } from "../../services"; import { Icon } from "./icon"; interface SortSelectProps { - sort: SortType; - onChange(val: SortType): void; + sort: PostSortType; + onChange(val: PostSortType): void; hideHot?: boolean; hideMostComments?: boolean; } interface SortSelectState { - sort: SortType; + sort: PostSortType; } export class SortSelect extends Component { @@ -47,55 +47,53 @@ export class SortSelect extends Component { {I18NextService.i18n.t("sort_type")} {!this.props.hideHot && [ - , - , - , ]} - - - + + {!this.props.hideMostComments && [ - , - , ]} - - + - - - - - + + + - - - - + + { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); + this.handleCommentSortChange = this.handleCommentSortChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handlePageNext = this.handlePageNext.bind(this); this.handlePagePrev = this.handlePagePrev.bind(this); @@ -628,7 +632,14 @@ export class Community extends Component { )} - + {this.props.dataType === DataType.Post ? ( + + ) : ( + + )} {communityRss && ( <> @@ -654,8 +665,18 @@ export class Community extends Component { this.updateUrl({ pageCursor: nextPage }); } - handleSortChange(sort: SortType) { - this.updateUrl({ sort, pageCursor: undefined }); + handleSortChange(sort: PostSortType) { + this.updateUrl({ + sort: sort, + pageCursor: undefined, + }); + } + + handleCommentSortChange(sort: CommentSortType) { + this.updateUrl({ + sort: commentToPostSortType(sort), + pageCursor: undefined, + }); } handleDataTypeChange(dataType: DataType) { diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx index f33b0943..d36df775 100644 --- a/src/shared/components/home/admin-settings.tsx +++ b/src/shared/components/home/admin-settings.tsx @@ -6,9 +6,6 @@ import classNames from "classnames"; import { Component } from "inferno"; import { BannedPersonsResponse, - CreateCustomEmoji, - DeleteCustomEmoji, - EditCustomEmoji, EditSite, GetFederatedInstancesResponse, GetSiteResponse, @@ -17,7 +14,6 @@ import { PersonView, } from "lemmy-js-client"; import { InitialFetchRequest } from "../../interfaces"; -import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown"; import { FirstLoadService, I18NextService } from "../../services"; import { EMPTY_REQUEST, @@ -104,9 +100,6 @@ export class AdminSettings extends Component< super(props, context); this.handleEditSite = this.handleEditSite.bind(this); - this.handleEditEmoji = this.handleEditEmoji.bind(this); - this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this); - this.handleCreateEmoji = this.handleCreateEmoji.bind(this); this.handleUploadsPageChange = this.handleUploadsPageChange.bind(this); this.handleToggleShowLeaveAdminConfirmation = this.handleToggleShowLeaveAdminConfirmation.bind(this); @@ -249,11 +242,7 @@ export class AdminSettings extends Component< id="taglines-tab-pane" >
- +
), @@ -270,11 +259,7 @@ export class AdminSettings extends Component< id="emojis-tab-pane" >
- +
), @@ -431,7 +416,6 @@ export class AdminSettings extends Component< this.setState(s => { s.siteRes.site_view = editRes.data.site_view; // TODO: Where to get taglines from? - s.siteRes.taglines = editRes.data.taglines; return s; }); toast(I18NextService.i18n.t("site_saved")); @@ -464,27 +448,6 @@ export class AdminSettings extends Component< } } - async handleEditEmoji(form: EditCustomEmoji) { - const res = await HttpService.client.editCustomEmoji(form); - if (res.state === "success") { - updateEmojiDataModel(res.data.custom_emoji); - } - } - - async handleDeleteEmoji(form: DeleteCustomEmoji) { - const res = await HttpService.client.deleteCustomEmoji(form); - if (res.state === "success") { - removeFromEmojiDataModel(form.id); - } - } - - async handleCreateEmoji(form: CreateCustomEmoji) { - const res = await HttpService.client.createCustomEmoji(form); - if (res.state === "success") { - updateEmojiDataModel(res.data.custom_emoji); - } - } - async handleUploadsPageChange(val: number) { this.setState({ uploadsPage: val }); snapToTop(); diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx index e6d299e6..79ae4b51 100644 --- a/src/shared/components/home/emojis-form.tsx +++ b/src/shared/components/home/emojis-form.tsx @@ -1,79 +1,81 @@ -import { setIsoData } from "@utils/app"; import { capitalizeFirstLetter } from "@utils/helpers"; import { Component, linkEvent } from "inferno"; -import { - CreateCustomEmoji, - DeleteCustomEmoji, - EditCustomEmoji, - GetSiteResponse, -} from "lemmy-js-client"; -import { customEmojisLookup } from "../../markdown"; +import { CustomEmojiView } from "lemmy-js-client"; +import { emojiMartCategories, EmojiMartCategory } from "../../markdown"; import { HttpService, I18NextService } from "../../services"; import { pictrsDeleteToast, toast } from "../../toast"; import { EmojiMart } from "../common/emoji-mart"; import { Icon, Spinner } from "../common/icon"; import { Paginator } from "../common/paginator"; import { tippyMixin } from "../mixins/tippy-mixin"; +import { isBrowser } from "@utils/browser"; +import classNames from "classnames"; +import { amAdmin } from "@utils/roles"; +import { Prompt } from "inferno-router"; -interface EmojiFormProps { - onEdit(form: EditCustomEmoji): void; - onCreate(form: CreateCustomEmoji): void; - onDelete(form: DeleteCustomEmoji): void; +interface EditableEmoji { + change?: "update" | "delete" | "create"; + emoji: CustomEmojiView; + loading?: boolean; +} + +function markForUpdate(editable: EditableEmoji) { + if (editable.change !== "create") { + editable.change = "update"; + } } interface EmojiFormState { - siteRes: GetSiteResponse; - customEmojis: CustomEmojiViewForm[]; - page: number; -} - -interface CustomEmojiViewForm { - id: number; - category: string; - shortcode: string; - image_url: string; - alt_text: string; - keywords: string; - changed: boolean; + emojis: EditableEmoji[]; // Emojis for the current page + allEmojis: CustomEmojiView[]; // All emojis for emoji lookup across pages + emojiMartCustom: EmojiMartCategory[]; + emojiMartKey: number; page: number; loading: boolean; } @tippyMixin -export class EmojiForm extends Component { - private isoData = setIsoData(this.context); +export class EmojiForm extends Component, EmojiFormState> { private itemsPerPage = 15; - private emptyState: EmojiFormState = { - siteRes: this.isoData.site_res, - customEmojis: this.isoData.site_res.custom_emojis.map((x, index) => ({ - id: x.custom_emoji.id, - category: x.custom_emoji.category, - shortcode: x.custom_emoji.shortcode, - image_url: x.custom_emoji.image_url, - alt_text: x.custom_emoji.alt_text, - keywords: x.keywords.map(x => x.keyword).join(" "), - changed: false, - page: 1 + Math.floor(index / this.itemsPerPage), - loading: false, - })), + private needsRefetch = true; + state: EmojiFormState = { + emojis: [], + allEmojis: [], + emojiMartCustom: [], + emojiMartKey: 1, + loading: false, page: 1, }; - state: EmojiFormState; private scrollRef: any = {}; constructor(props: any, context: any) { super(props, context); - this.state = this.emptyState; this.handlePageChange = this.handlePageChange.bind(this); this.handleEmojiClick = this.handleEmojiClick.bind(this); } + + async componentWillMount() { + if (isBrowser()) { + this.handlePageChange(1); + } + } + + hasPendingChanges() { + return this.state.emojis.some(x => x.change); + } + render() { return (
+

{I18NextService.i18n.t("custom_emojis")}

- {customEmojisLookup.size > 0 && ( + {this.state.emojiMartCustom.length > 0 && (
@@ -87,6 +89,10 @@ export class EmojiForm extends Component { {I18NextService.i18n.t("column_emoji")} + {I18NextService.i18n.t("column_shortcode")} @@ -102,20 +108,15 @@ export class EmojiForm extends Component { {I18NextService.i18n.t("column_keywords")} + - {this.state.customEmojis - .slice( - Number((this.state.page - 1) * this.itemsPerPage), - Number( - (this.state.page - 1) * this.itemsPerPage + - this.itemsPerPage, - ), - ) - .map((cv, index) => ( - (this.scrollRef[cv.shortcode] = e)}> + {this.state.emojis.map((editable: EditableEmoji, index) => { + const cv = editable.emoji.custom_emoji; + return ( + {cv.image_url.length > 0 && ( { alt={cv.alt_text} /> )} - {cv.image_url.length === 0 && ( + + + { - )} + } { (this.scrollRef[cv.shortcode] = e)} type="text" placeholder="Category" className="form-control" @@ -206,31 +210,54 @@ export class EmojiForm extends Component { type="text" placeholder="Keywords" className="form-control" - value={cv.keywords} + value={editable.emoji.keywords + .map(k => k.keyword) + .join(" ")} onInput={linkEvent( { form: this, index: index }, this.handleEmojiKeywordChange, )} /> + + {editable.change === "update" && ( + + + + )} + {editable.change === "delete" && ( + + + + )} + {editable.change === "create" && ( + + + + )} + -
- +
+ +
- ))} + ); + })}
@@ -273,43 +318,91 @@ export class EmojiForm extends Component { page={this.state.page} onChange={this.handlePageChange} nextDisabled={false} + disabled={this.hasPendingChanges()} />
); } - canEdit(cv: CustomEmojiViewForm) { - const noEmptyFields = - cv.alt_text.length > 0 && - cv.category.length > 0 && - cv.image_url.length > 0 && - cv.shortcode.length > 0; - const noDuplicateShortCodes = - this.state.customEmojis.filter( - x => x.shortcode === cv.shortcode && x.id !== cv.id, - ).length === 0; - return noEmptyFields && noDuplicateShortCodes && !cv.loading && cv.changed; + canSave(cv: EditableEmoji) { + const requiredFields = + cv.emoji.custom_emoji.image_url.length > 0 && + cv.emoji.custom_emoji.shortcode.length > 0; + return requiredFields && !cv.loading; } - getEditTooltip(cv: CustomEmojiViewForm) { - if (this.canEdit(cv)) return I18NextService.i18n.t("save"); + getEditTooltip(cv: EditableEmoji) { + if (this.canSave(cv)) return I18NextService.i18n.t("save"); else return I18NextService.i18n.t("custom_emoji_save_validation"); } - handlePageChange(page: number) { - this.setState({ page: page }); + async handlePageChange(page: number) { + this.setState({ loading: true }); + let allEmojis: CustomEmojiView[] = this.state.allEmojis; + let emojiMartCustom: EmojiMartCategory[] = this.state.emojiMartCustom; + let emojiMartKey: number = this.state.emojiMartKey; + if (this.needsRefetch) { + const emojiRes = await HttpService.client.listCustomEmojis({ + ignore_page_limits: true, + }); + if (emojiRes.state === "success") { + this.needsRefetch = false; + allEmojis = emojiRes.data.custom_emojis; + allEmojis.sort((a, b) => { + const categoryOrder = a.custom_emoji.category.localeCompare( + b.custom_emoji.category, + ); + if (categoryOrder === 0) { + return a.custom_emoji.shortcode.localeCompare( + b.custom_emoji.shortcode, + ); + } + return categoryOrder; + }); + } + emojiMartCustom = emojiMartCategories(allEmojis); + emojiMartKey++; + } + if (allEmojis) { + const startIndex = (page - 1) * this.itemsPerPage; + const emojis = allEmojis + .slice(startIndex, startIndex + this.itemsPerPage) + .map(x => ({ emoji: structuredClone(x) })); // clone for restore after cancel + this.setState({ + loading: false, + allEmojis, + emojiMartCustom, + emojiMartKey, + emojis, + page, + }); + } else { + this.setState({ loading: false, page }); + } } - handleEmojiClick(e: any) { - const view = customEmojisLookup.get(e.id); - if (view) { - const page = this.state.customEmojis.find( - x => x.id === view.custom_emoji.id, - )?.page; - if (page) { - this.setState({ page: page }); - this.scrollRef[view.custom_emoji.shortcode].scrollIntoView(); + async handleEmojiClick(e: any) { + const emojiIndex = this.state.allEmojis.findIndex( + x => x.custom_emoji.shortcode === e.id, + ); + if (emojiIndex >= 0) { + const { shortcode } = this.state.allEmojis[emojiIndex].custom_emoji; + const page = Math.floor(emojiIndex / this.itemsPerPage) + 1; + if (page !== this.state.page) { + if ( + this.hasPendingChanges() && + !confirm(I18NextService.i18n.t("block_leaving")) + ) { + return; + } + await this.handlePageChange(page); + await new Promise(r => setTimeout(r)); + } + if (shortcode) { + const categoryInput: HTMLInputElement | undefined = + this.scrollRef[shortcode]; + categoryInput?.focus(); } } } @@ -318,32 +411,22 @@ export class EmojiForm extends Component { props: { form: EmojiForm; index: number }, event: any, ) { - const custom_emojis = [...props.form.state.customEmojis]; - const pagedIndex = - (props.form.state.page - 1) * props.form.itemsPerPage + props.index; - const item = { - ...props.form.state.customEmojis[pagedIndex], - category: event.target.value, - changed: true, - }; - custom_emojis[Number(pagedIndex)] = item; - props.form.setState({ customEmojis: custom_emojis }); + const editable: EditableEmoji = props.form.state.emojis[props.index]; + props.form.setState(() => { + markForUpdate(editable); + editable.emoji.custom_emoji.category = event.target.value; + }); } handleEmojiShortCodeChange( props: { form: EmojiForm; index: number }, event: any, ) { - const custom_emojis = [...props.form.state.customEmojis]; - const pagedIndex = - (props.form.state.page - 1) * props.form.itemsPerPage + props.index; - const item = { - ...props.form.state.customEmojis[pagedIndex], - shortcode: event.target.value, - changed: true, - }; - custom_emojis[Number(pagedIndex)] = item; - props.form.setState({ customEmojis: custom_emojis }); + const editable: EditableEmoji = props.form.state.emojis[props.index]; + props.form.setState(() => { + markForUpdate(editable); + editable.emoji.custom_emoji.shortcode = event.target.value; + }); } handleEmojiImageUrlChange( @@ -354,28 +437,11 @@ export class EmojiForm extends Component { }: { form: EmojiForm; index: number; overrideValue: string | null }, event: any, ) { - form.setState(prevState => { - const custom_emojis = [...form.state.customEmojis]; - const pagedIndex = (form.state.page - 1) * form.itemsPerPage + index; - const item = { - ...form.state.customEmojis[pagedIndex], - image_url: overrideValue ?? event.target.value, - changed: true, - }; - custom_emojis[Number(pagedIndex)] = item; - return { - ...prevState, - customEmojis: prevState.customEmojis.map((ce, i) => - i === pagedIndex - ? { - ...ce, - image_url: overrideValue ?? event.target.value, - changed: true, - loading: false, - } - : ce, - ), - }; + const editable: EditableEmoji = form.state.emojis[index]; + form.setState(() => { + markForUpdate(editable); + editable.emoji.custom_emoji.image_url = + overrideValue ?? event.target.value; }); } @@ -383,97 +449,117 @@ export class EmojiForm extends Component { props: { form: EmojiForm; index: number }, event: any, ) { - const custom_emojis = [...props.form.state.customEmojis]; - const pagedIndex = - (props.form.state.page - 1) * props.form.itemsPerPage + props.index; - const item = { - ...props.form.state.customEmojis[pagedIndex], - alt_text: event.target.value, - changed: true, - }; - custom_emojis[Number(pagedIndex)] = item; - props.form.setState({ customEmojis: custom_emojis }); + const editable: EditableEmoji = props.form.state.emojis[props.index]; + props.form.setState(() => { + markForUpdate(editable); + editable.emoji.custom_emoji.alt_text = event.target.value; + }); } handleEmojiKeywordChange( props: { form: EmojiForm; index: number }, event: any, ) { - const custom_emojis = [...props.form.state.customEmojis]; - const pagedIndex = - (props.form.state.page - 1) * props.form.itemsPerPage + props.index; - const item = { - ...props.form.state.customEmojis[pagedIndex], - keywords: event.target.value, - changed: true, - }; - custom_emojis[Number(pagedIndex)] = item; - props.form.setState({ customEmojis: custom_emojis }); + const editable: EditableEmoji = props.form.state.emojis[props.index]; + props.form.setState(() => { + markForUpdate(editable); + editable.emoji.keywords = event.target.value + .split(" ") + .map((x: string) => ({ id: -1, keyword: x })); + }); } handleDeleteEmojiClick(d: { i: EmojiForm; index: number; - cv: CustomEmojiViewForm; + cv: EditableEmoji; }) { - const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index; - if (d.cv.id !== 0) { - d.i.props.onDelete({ - id: d.cv.id, - }); + if (d.cv.change === "create") { + // This drops the entry immediately, other deletes have to be saved. + d.i.setState(prev => ({ + emojis: prev.emojis.filter(x => x !== d.cv), + })); } else { - const custom_emojis = [...d.i.state.customEmojis]; - custom_emojis.splice(Number(pagedIndex), 1); - d.i.setState({ customEmojis: custom_emojis }); - } - } - - handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) { - const keywords = d.cv.keywords - .split(" ") - .filter(x => x.length > 0) as string[]; - const uniqueKeywords = Array.from(new Set(keywords)); - if (d.cv.id !== 0) { - d.i.props.onEdit({ - id: d.cv.id, - category: d.cv.category, - image_url: d.cv.image_url, - alt_text: d.cv.alt_text, - keywords: uniqueKeywords, - }); - } else { - d.i.props.onCreate({ - category: d.cv.category, - shortcode: d.cv.shortcode, - image_url: d.cv.image_url, - alt_text: d.cv.alt_text, - keywords: uniqueKeywords, + d.i.setState(() => { + d.cv.change = "delete"; }); } } - handleAddEmojiClick(form: EmojiForm, event: any) { + async handleSaveEmojiClick(d: { i: EmojiForm; cv: EditableEmoji }) { + d.i.needsRefetch = true; + const editable = d.cv; + if (editable.change === "update") { + const resp = await HttpService.client.editCustomEmoji({ + ...editable.emoji.custom_emoji, + keywords: editable.emoji.keywords.map(x => x.keyword), + }); + if (resp.state === "success") { + d.i.setState(() => { + editable.emoji = resp.data.custom_emoji; + editable.change = undefined; + }); + } + } else if (editable.change === "delete") { + const resp = await HttpService.client.deleteCustomEmoji( + editable.emoji.custom_emoji, + ); + if (resp.state === "success") { + d.i.setState(prev => ({ + emojis: prev.emojis.filter(x => x !== editable), + })); + } + } else if (editable.change === "create") { + const resp = await HttpService.client.createCustomEmoji({ + ...editable.emoji.custom_emoji, + keywords: editable.emoji.keywords.map(x => x.keyword), + }); + if (resp.state === "success") { + d.i.setState(() => { + editable.emoji = resp.data.custom_emoji; + editable.change = undefined; + }); + } + } + } + + async handleCancelEmojiClick(d: { i: EmojiForm; cv: EditableEmoji }) { + if (d.cv.change === "create") { + d.i.setState(() => { + return { + emojis: d.i.state.emojis.filter(x => x !== d.cv), + }; + }); + } else if (d.cv.change === "update" || d.cv.change === "delete") { + const original = d.i.state.allEmojis.find( + x => x.custom_emoji.id === d.cv.emoji.custom_emoji.id, + ); + if (original) { + d.i.setState(() => { + d.cv.emoji = structuredClone(original); + d.cv.change = undefined; + }); + } + } + } + + async handleAddEmojiClick(form: EmojiForm, event: any) { event.preventDefault(); - form.setState(prevState => { - const page = - 1 + Math.floor(prevState.customEmojis.length / form.itemsPerPage); - const item: CustomEmojiViewForm = { - id: 0, - shortcode: "", - alt_text: "", - category: "", - image_url: "", - keywords: "", - changed: false, - page: page, - loading: false, - }; - - return { - ...prevState, - customEmojis: [...prevState.customEmojis, item], - page, - }; + form.setState(prev => { + prev.emojis.push({ + emoji: { + custom_emoji: { + id: -1, + published: "", + category: "", + shortcode: "", + image_url: "", + alt_text: "", + }, + keywords: [], + }, + change: "create", + }); }); } @@ -489,14 +575,15 @@ export class EmojiForm extends Component { file = event; } - form.setState(prevState => ({ - ...prevState, - customEmojis: prevState.customEmojis.map((cv, i) => - i === index ? { ...cv, loading: true } : cv, - ), - })); + const editable = form.state.emojis[index]; + form.setState(() => { + editable.loading = true; + }); HttpService.client.uploadImage({ image: file }).then(res => { + form.setState(() => { + editable.loading = false; + }); if (res.state === "success") { if (res.data.msg === "ok") { pictrsDeleteToast(file.name, res.data.delete_url as string); @@ -517,10 +604,20 @@ export class EmojiForm extends Component { } configurePicker(): any { + const custom = this.state.emojiMartCustom; + if (process.env["NODE_ENV"] === "development") { + // Once an emoji-mart Picker is initialized with these options, other + // instances also only show the custom emojis. + console.assert( + amAdmin(), + "EmojiMart doesn't deal well with differently configured instances.", + ); + } return { data: { categories: [], emojis: [], aliases: [] }, maxFrequentRows: 0, dynamicWidth: true, + custom, }; } } diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx index 52ec91a4..0e1f451f 100644 --- a/src/shared/components/home/home.tsx +++ b/src/shared/components/home/home.tsx @@ -1,5 +1,6 @@ import { commentsToFlatNodes, + commentToPostSortType, editComment, editPost, editWith, @@ -16,7 +17,6 @@ import { import { getQueryParams, getQueryString, - getRandomFromList, resourcesSettled, } from "@utils/helpers"; import { scrollMixin } from "../mixins/scroll-mixin"; @@ -67,9 +67,10 @@ import { RemovePost, SaveComment, SavePost, - SortType, + PostSortType, SuccessResponse, TransferCommunity, + CommentSortType, } from "lemmy-js-client"; import { fetchLimit, relTags } from "../../config"; import { @@ -107,6 +108,7 @@ import { RouteComponentProps } from "inferno-router/dist/Route"; import { IRoutePropsWithFetch } from "../../routes"; import PostHiddenSelect from "../common/post-hidden-select"; import { isBrowser, snapToTop } from "@utils/browser"; +import { CommentSortSelect } from "../common/comment-sort-select"; interface HomeState { postsRes: RequestState; @@ -122,7 +124,7 @@ interface HomeState { interface HomeProps { listingType?: ListingType; dataType: DataType; - sort: SortType; + sort: PostSortType; pageCursor?: PaginationCursor; showHidden?: StringBoolean; } @@ -132,7 +134,7 @@ type HomeData = RouteDataResponse<{ commentsRes: GetCommentsResponse; }>; -function getRss(listingType: ListingType, sort: SortType) { +function getRss(listingType: ListingType, sort: PostSortType) { let rss: string | undefined = undefined; const queryString = getQueryString({ sort }); @@ -177,13 +179,13 @@ function getListingTypeFromQuery( function getSortTypeFromQuery( type: string | undefined, - fallback: SortType, -): SortType { - return type ? (type as SortType) : fallback; + fallback: PostSortType, +): PostSortType { + return type ? (type as PostSortType) : fallback; } type Fallbacks = { - sort: SortType; + sort: PostSortType; listingType: ListingType; }; @@ -204,7 +206,8 @@ export function getHomeQueryParams( }, source, { - sort: local_user?.default_sort_type ?? local_site.default_sort_type, + sort: + local_user?.default_post_sort_type ?? local_site.default_post_sort_type, listingType: local_user?.default_listing_type ?? local_site.default_post_listing_type, @@ -264,6 +267,7 @@ export class Home extends Component { super(props, context); this.handleSortChange = this.handleSortChange.bind(this); + this.handleCommentSortChange = this.handleCommentSortChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleDataTypeChange = this.handleDataTypeChange.bind(this); this.handleShowHiddenChange = this.handleShowHiddenChange.bind(this); @@ -311,9 +315,7 @@ export class Home extends Component { }; } - this.state.tagline = getRandomFromList( - this.state?.siteRes?.taglines ?? [], - )?.content; + this.state.tagline = this.state?.siteRes?.tagline?.content; } async componentWillMount() { @@ -726,7 +728,14 @@ export class Home extends Component { />
- + {this.props.dataType === DataType.Post ? ( + + ) : ( + + )}
{getRss( @@ -799,10 +808,14 @@ export class Home extends Component { this.updateUrl({ pageCursor: nextPage }); } - handleSortChange(val: SortType) { + handleSortChange(val: PostSortType) { this.updateUrl({ sort: val, pageCursor: undefined }); } + handleCommentSortChange(val: CommentSortType) { + this.updateUrl({ sort: commentToPostSortType(val), pageCursor: undefined }); + } + handleListingTypeChange(val: ListingType) { this.updateUrl({ listingType: val, pageCursor: undefined }); } diff --git a/src/shared/components/home/site-form.tsx b/src/shared/components/home/site-form.tsx index 5eac726a..b2d050cc 100644 --- a/src/shared/components/home/site-form.tsx +++ b/src/shared/components/home/site-form.tsx @@ -2,7 +2,6 @@ import { capitalizeFirstLetter, validInstanceTLD } from "@utils/helpers"; import { Component, InfernoKeyboardEvent, - InfernoMouseEvent, InfernoNode, linkEvent, } from "inferno"; @@ -877,42 +876,6 @@ export class SiteForm extends Component { this.setState(s => ((s.siteForm.legal_information = val), s)); } - handleTaglineChange(i: SiteForm, index: number, val: string) { - const taglines = i.state.siteForm.taglines; - if (taglines) { - taglines[index] = val; - i.setState(i.state); - } - } - - handleDeleteTaglineClick( - i: SiteForm, - index: number, - event: InfernoMouseEvent, - ) { - event.preventDefault(); - const taglines = i.state.siteForm.taglines; - if (taglines) { - taglines.splice(index, 1); - i.state.siteForm.taglines = undefined; - i.setState(i.state); - i.state.siteForm.taglines = taglines; - i.setState(i.state); - } - } - - handleAddTaglineClick( - i: SiteForm, - event: InfernoMouseEvent, - ) { - event.preventDefault(); - if (!i.state.siteForm.taglines) { - i.state.siteForm.taglines = []; - } - i.state.siteForm.taglines.push(""); - i.setState(i.state); - } - handleSiteApplicationQuestionChange(val: string) { this.setState(s => ((s.siteForm.application_question = val), s)); } diff --git a/src/shared/components/home/tagline-form.tsx b/src/shared/components/home/tagline-form.tsx index ed93f466..5c08ce1d 100644 --- a/src/shared/components/home/tagline-form.tsx +++ b/src/shared/components/home/tagline-form.tsx @@ -1,49 +1,84 @@ import { capitalizeFirstLetter } from "@utils/helpers"; import { Component, InfernoMouseEvent, linkEvent } from "inferno"; -import { EditSite, Tagline } from "lemmy-js-client"; -import { I18NextService } from "../../services"; +import { Tagline } from "lemmy-js-client"; +import { HttpService, I18NextService } from "../../services"; import { Icon, Spinner } from "../common/icon"; import { MarkdownTextArea } from "../common/markdown-textarea"; import { tippyMixin } from "../mixins/tippy-mixin"; +import { Paginator } from "../common/paginator"; +import classNames from "classnames"; +import { isBrowser } from "@utils/browser"; +import { Prompt } from "inferno-router"; -interface TaglineFormProps { - taglines: Array; - onSaveSite(form: EditSite): void; - loading: boolean; +interface EditableTagline { + change?: "update" | "delete" | "create"; + editMode?: boolean; + tagline: Tagline; +} + +function markForUpdate(editable: EditableTagline) { + if (editable.change !== "create") { + editable.change = "update"; + } } interface TaglineFormState { - taglines: Array; - editingRow?: number; + taglines: Array; + page: number; + loading: boolean; } @tippyMixin -export class TaglineForm extends Component { +export class TaglineForm extends Component< + Record, + TaglineFormState +> { state: TaglineFormState = { - editingRow: undefined, - taglines: this.props.taglines.map(x => x.content), + taglines: [], + page: 1, + loading: false, }; constructor(props: any, context: any) { super(props, context); + this.handlePageChange = this.handlePageChange.bind(this); + } + + componentWillMount(): void { + if (isBrowser()) { + this.handlePageChange(1); + } + } + + hasPendingChanges(): boolean { + return this.state.taglines.some(x => x.change); } render() { return (
+

{I18NextService.i18n.t("taglines")}

- +
+ {this.state.taglines.map((cv, index) => ( +
- {this.state.editingRow === index && ( + {cv.editMode ? ( this.handleTaglineChange(this, index, s) } @@ -51,8 +86,32 @@ export class TaglineForm extends Component { allLanguages={[]} siteLanguages={[]} /> + ) : ( +
{cv.tagline.content}
+ )} +
+ {cv.change === "update" && ( + + + + )} + {cv.change === "delete" && ( + + + + )} + {cv.change === "create" && ( + + + )} - {this.state.editingRow !== index &&
{cv}
}
+ {this.hasPendingChanges() && ( + + )} +
+ +
); } handleTaglineChange(i: TaglineForm, index: number, val: string) { - if (i.state.taglines) { - i.setState(prev => ({ - ...prev, - taglines: prev.taglines.map((tl, i) => (i === index ? val : tl)), - })); - } + const editable = i.state.taglines[index]; + i.setState(() => { + markForUpdate(editable); + const tagline: Tagline = editable.tagline; + tagline.content = val; + }); } - handleDeleteTaglineClick(d: { i: TaglineForm; index: number }, event: any) { + async handleDeleteTaglineClick( + d: { i: TaglineForm; index: number }, + event: any, + ) { event.preventDefault(); - d.i.setState(prev => ({ - ...prev, - taglines: prev.taglines.filter((_, i) => i !== d.index), - editingRow: undefined, - })); + const editable = d.i.state.taglines[d.index]; + if (editable.change === "create") { + // This drops the entry immediately, other deletes have to be saved. + d.i.setState(prev => { + return { taglines: prev.taglines.filter(x => x !== editable) }; + }); + } else { + d.i.setState(() => { + editable.change = "delete"; + editable.editMode = false; + }); + } } handleEditTaglineClick(d: { i: TaglineForm; index: number }, event: any) { event.preventDefault(); - if (d.i.state.editingRow === d.index) { - d.i.setState({ editingRow: undefined }); - } else { - d.i.setState({ editingRow: d.index }); - } - } - - async handleSaveClick(i: TaglineForm) { - i.props.onSaveSite({ - taglines: i.state.taglines, + const editable = d.i.state.taglines[d.index]; + d.i.setState(prev => { + prev.taglines + .filter(x => x !== editable) + .forEach(x => { + x.editMode = false; + }); + editable.editMode = !editable.editMode; }); } - handleAddTaglineClick( + async handleSaveClick(i: TaglineForm) { + const promises: Promise[] = []; + for (const editable of i.state.taglines) { + if (editable.change === "update") { + promises.push( + HttpService.client.editTagline(editable.tagline).then(res => { + if (res.state === "success") { + i.setState(() => { + editable.change = undefined; + editable.tagline = res.data.tagline; + }); + } + }), + ); + } else if (editable.change === "delete") { + promises.push( + HttpService.client.deleteTagline(editable.tagline).then(res => { + if (res.state === "success") { + i.setState(() => { + editable.change = undefined; + return { + taglines: this.state.taglines.filter(x => x !== editable), + }; + }); + } + }), + ); + } else if (editable.change === "create") { + promises.push( + HttpService.client.createTagline(editable.tagline).then(res => { + if (res.state === "success") { + i.setState(() => { + editable.change = undefined; + editable.tagline = res.data.tagline; + }); + } + }), + ); + } + } + await Promise.all(promises); + } + + async handleCancelClick(i: TaglineForm) { + i.handlePageChange(i.state.page); + } + + async handleAddTaglineClick( i: TaglineForm, event: InfernoMouseEvent, ) { event.preventDefault(); - const newTaglines = [...i.state.taglines]; - newTaglines.push(""); - - i.setState({ - taglines: newTaglines, - editingRow: newTaglines.length - 1, + i.setState(prev => { + prev.taglines.forEach(x => { + x.editMode = false; + }); + prev.taglines.push({ + tagline: { id: -1, content: "", published: "" }, + change: "create", + editMode: true, + }); }); } + + async handlePageChange(val: number) { + this.setState({ loading: true }); + const taglineRes = await HttpService.client.listTaglines({ page: val }); + if (taglineRes.state === "success") { + this.setState({ + page: val, + loading: false, + taglines: taglineRes.data.taglines.map(t => ({ tagline: t })), + }); + } else { + this.setState({ loading: false }); + } + } } diff --git a/src/shared/components/person/person-details.tsx b/src/shared/components/person/person-details.tsx index 67aab710..5667a572 100644 --- a/src/shared/components/person/person-details.tsx +++ b/src/shared/components/person/person-details.tsx @@ -37,7 +37,7 @@ import { RemovePost, SaveComment, SavePost, - SortType, + PostSortType, TransferCommunity, } from "lemmy-js-client"; import { CommentViewType, PersonDetailsView } from "../../interfaces"; @@ -53,7 +53,7 @@ interface PersonDetailsProps { siteLanguages: number[]; page: number; limit: number; - sort: SortType; + sort: PostSortType; enableDownvotes: boolean; voteDisplayMode: LocalUserVoteDisplayMode; enableNsfw: boolean; diff --git a/src/shared/components/person/profile.tsx b/src/shared/components/person/profile.tsx index 04f0445d..138867ac 100644 --- a/src/shared/components/person/profile.tsx +++ b/src/shared/components/person/profile.tsx @@ -70,7 +70,7 @@ import { RemovePost, SaveComment, SavePost, - SortType, + PostSortType, SuccessResponse, TransferCommunity, RegistrationApplicationResponse, @@ -127,7 +127,7 @@ interface ProfileState { interface ProfileProps { view: PersonDetailsView; - sort: SortType; + sort: PostSortType; page: number; } @@ -142,8 +142,8 @@ export function getProfileQueryParams(source?: string): ProfileProps { ); } -function getSortTypeFromQuery(sort?: string): SortType { - return sort ? (sort as SortType) : "New"; +function getSortTypeFromQuery(sort?: string): PostSortType { + return sort ? (sort as PostSortType) : "New"; } function getViewFromProps(view?: string): PersonDetailsView { @@ -956,7 +956,7 @@ export class Profile extends Component { this.updateUrl({ page }); } - handleSortChange(sort: SortType) { + handleSortChange(sort: PostSortType) { this.updateUrl({ sort, page: 1 }); } diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx index cffd93a1..ddd13afd 100644 --- a/src/shared/components/person/settings.tsx +++ b/src/shared/components/person/settings.tsx @@ -22,6 +22,7 @@ import { BlockCommunityResponse, BlockInstanceResponse, BlockPersonResponse, + CommentSortType, Community, GenerateTotpSecretResponse, GetFederatedInstancesResponse, @@ -31,7 +32,7 @@ import { ListingType, LoginResponse, Person, - SortType, + PostSortType, SuccessResponse, UpdateTotpResponse, } from "lemmy-js-client"; @@ -76,6 +77,7 @@ import { getHttpBaseInternal } from "../../utils/env"; import { IRoutePropsWithFetch } from "../../routes"; import { RouteComponentProps } from "inferno-router/dist/Route"; import { simpleScrollMixin } from "../mixins/scroll-mixin"; +import { CommentSortSelect } from "../common/comment-sort-select"; type SettingsData = RouteDataResponse<{ instancesRes: GetFederatedInstancesResponse; @@ -94,7 +96,8 @@ interface SettingsState { blur_nsfw?: boolean; auto_expand?: boolean; theme?: string; - default_sort_type?: SortType; + default_post_sort_type?: PostSortType; + default_comment_sort_type?: CommentSortType; default_listing_type?: ListingType; interface_language?: string; avatar?: string; @@ -248,7 +251,9 @@ export class Settings extends Component { constructor(props: any, context: any) { super(props, context); - this.handleSortTypeChange = this.handleSortTypeChange.bind(this); + this.handlePostSortTypeChange = this.handlePostSortTypeChange.bind(this); + this.handleCommentSortTypeChange = + this.handleCommentSortTypeChange.bind(this); this.handleListingTypeChange = this.handleListingTypeChange.bind(this); this.handleBioChange = this.handleBioChange.bind(this); this.handleDiscussionLanguageChange = @@ -276,9 +281,9 @@ export class Settings extends Component { local_user: { show_nsfw, blur_nsfw, - auto_expand, theme, - default_sort_type, + default_post_sort_type, + default_comment_sort_type, default_listing_type, interface_language, show_avatars, @@ -313,9 +318,9 @@ export class Settings extends Component { ...this.state.saveUserSettingsForm, show_nsfw, blur_nsfw, - auto_expand, theme: theme ?? "browser", - default_sort_type, + default_post_sort_type, + default_comment_sort_type, default_listing_type, interface_language, discussion_languages: mui.discussion_languages, @@ -905,14 +910,29 @@ export class Settings extends Component {
+
+
+
+ +
+
@@ -1580,8 +1600,16 @@ export class Settings extends Component { ); } - handleSortTypeChange(val: SortType) { - this.setState(s => ((s.saveUserSettingsForm.default_sort_type = val), s)); + handlePostSortTypeChange(val: PostSortType) { + this.setState( + s => ((s.saveUserSettingsForm.default_post_sort_type = val), s), + ); + } + + handleCommentSortTypeChange(val: CommentSortType) { + this.setState( + s => ((s.saveUserSettingsForm.default_comment_sort_type = val), s), + ); } handleListingTypeChange(val: ListingType) { @@ -1743,9 +1771,9 @@ export class Settings extends Component { local_user: { show_nsfw, blur_nsfw, - auto_expand, theme, - default_sort_type, + default_post_sort_type, + default_comment_sort_type, default_listing_type, interface_language, show_avatars, @@ -1782,11 +1810,11 @@ export class Settings extends Component { display_name, bio, matrix_user_id, - auto_expand, blur_nsfw, bot_account, default_listing_type, - default_sort_type, + default_post_sort_type, + default_comment_sort_type, discussion_languages: siteRes.data.my_user?.discussion_languages, email, interface_language, diff --git a/src/shared/components/post/post-listing.tsx b/src/shared/components/post/post-listing.tsx index 1a3bc750..3301fd7f 100644 --- a/src/shared/components/post/post-listing.tsx +++ b/src/shared/components/post/post-listing.tsx @@ -148,10 +148,10 @@ export class PostListing extends Component { UserService.Instance.myUserInfo && !this.isoData.showAdultConsentModal ) { - const { auto_expand, blur_nsfw } = - UserService.Instance.myUserInfo.local_user_view.local_user; + const blur_nsfw = + UserService.Instance.myUserInfo.local_user_view.local_user.blur_nsfw; this.setState({ - imageExpanded: auto_expand && !(blur_nsfw && this.postView.post.nsfw), + imageExpanded: !(blur_nsfw && this.postView.post.nsfw), }); } diff --git a/src/shared/components/post/post.tsx b/src/shared/components/post/post.tsx index 8b5fa535..53fada7a 100644 --- a/src/shared/components/post/post.tsx +++ b/src/shared/components/post/post.tsx @@ -124,11 +124,12 @@ interface PostState { lastCreatedCommentId?: CommentId; } -const defaultCommentSort: CommentSortType = "Hot"; - -function getCommentSortTypeFromQuery(source?: string): CommentSortType { +function getCommentSortTypeFromQuery( + source: string | undefined, + fallback: CommentSortType, +): CommentSortType { if (!source) { - return defaultCommentSort; + return fallback; } switch (source) { case "Hot": @@ -138,14 +139,21 @@ function getCommentSortTypeFromQuery(source?: string): CommentSortType { case "Controversial": return source; default: - return defaultCommentSort; + return fallback; } } function getQueryStringFromCommentSortType( sort: CommentSortType, + siteRes: GetSiteResponse, ): undefined | string { - if (sort === defaultCommentSort) { + const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo; + const local_user = myUserInfo?.local_user_view.local_user; + const local_site = siteRes.site_view.local_site; + const defaultSort = + local_user?.default_comment_sort_type ?? + local_site.default_comment_sort_type; + if (sort === defaultSort) { return undefined; } return sort; @@ -185,14 +193,31 @@ interface PostProps { view: CommentViewType; scrollToComments: boolean; } -export function getPostQueryParams(source: string | undefined): PostProps { - return getQueryParams( + +type Fallbacks = { + sort: CommentSortType; +}; + +export function getPostQueryParams( + source: string | undefined, + siteRes: GetSiteResponse, +): PostProps { + const myUserInfo = siteRes.my_user ?? UserService.Instance.myUserInfo; + const local_user = myUserInfo?.local_user_view.local_user; + const local_site = siteRes.site_view.local_site; + + return getQueryParams( { scrollToComments: (s?: string) => !!s, sort: getCommentSortTypeFromQuery, view: getCommentViewTypeFromQuery, }, source, + { + sort: + local_user?.default_comment_sort_type ?? + local_site.default_comment_sort_type, + }, ); } @@ -325,7 +350,7 @@ export class Post extends Component { }; const query: QueryParams = { - sort: getQueryStringFromCommentSortType(sort), + sort: getQueryStringFromCommentSortType(sort, this.state.siteRes), view: getQueryStringFromCommentView(view), }; diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx index e67442e0..a42ce446 100644 --- a/src/shared/components/search.tsx +++ b/src/shared/components/search.tsx @@ -46,7 +46,7 @@ import { Search as SearchForm, SearchResponse, SearchType, - SortType, + PostSortType, } from "lemmy-js-client"; import { fetchLimit } from "../config"; import { CommentViewType, InitialFetchRequest } from "../interfaces"; @@ -76,9 +76,9 @@ import { isBrowser } from "@utils/browser"; interface SearchProps { q?: string; type: SearchType; - sort: SortType; + sort: PostSortType; listingType: ListingType; - postTitleOnly?: boolean; + titleOnly?: boolean; communityId?: number; creatorId?: number; page: number; @@ -124,7 +124,7 @@ export function getSearchQueryParams(source?: string): SearchProps { type: getSearchTypeFromQuery, sort: getSortTypeFromQuery, listingType: getListingTypeFromQuery, - postTitleOnly: getBoolFromString, + titleOnly: getBoolFromString, communityId: getIdFromString, creatorId: getIdFromString, page: getPageFromString, @@ -139,8 +139,8 @@ function getSearchTypeFromQuery(type_?: string): SearchType { return type_ ? (type_ as SearchType) : defaultSearchType; } -function getSortTypeFromQuery(sort?: string): SortType { - return sort ? (sort as SortType) : defaultSortType; +function getSortTypeFromQuery(sort?: string): PostSortType { + return sort ? (sort as PostSortType) : defaultSortType; } function getListingTypeFromQuery(listingType?: string): ListingType { @@ -286,7 +286,7 @@ export class Search extends Component { this.handleCommunityFilterChange = this.handleCommunityFilterChange.bind(this); this.handleCreatorFilterChange = this.handleCreatorFilterChange.bind(this); - this.handlePostTitleChange = this.handlePostTitleChange.bind(this); + this.handleTitleOnlyChange = this.handleTitleOnlyChange.bind(this); // Only fetch the data if coming from another route if (FirstLoadService.isFirstLoad) { @@ -473,7 +473,7 @@ export class Search extends Component { type: searchType, sort, listingType: listing_type, - postTitleOnly: post_title_only, + titleOnly: title_only, communityId: community_id, creatorId: creator_id, page, @@ -519,7 +519,7 @@ export class Search extends Component { type_: searchType, sort, listing_type, - post_title_only, + title_only, page, limit: fetchLimit, }; @@ -595,7 +595,6 @@ export class Search extends Component { case "Comments": return this.comments; case "Posts": - case "Url": return this.posts; case "Communities": return this.communities; @@ -641,7 +640,7 @@ export class Search extends Component { } get selects() { - const { type, listingType, postTitleOnly, sort, communityId, creatorId } = + const { type, listingType, titleOnly, sort, communityId, creatorId } = this.props; const { communitySearchOptions, @@ -684,15 +683,12 @@ export class Search extends Component {
-
@@ -1074,7 +1070,7 @@ export class Search extends Component { type, sort, listingType, - postTitleOnly, + titleOnly, page, } = props; @@ -1087,7 +1083,7 @@ export class Search extends Component { type_: type, sort, listing_type: listingType, - post_title_only: postTitleOnly, + title_only: titleOnly, page, limit: fetchLimit, }); @@ -1152,13 +1148,13 @@ export class Search extends Component { return this.searchInput.current?.value ?? this.props.q; } - handleSortChange(sort: SortType) { + handleSortChange(sort: PostSortType) { this.updateUrl({ sort, page: 1, q: this.getQ() }); } - handlePostTitleChange(event: any) { - const postTitleOnly = event.target.checked; - this.updateUrl({ postTitleOnly, q: this.getQ() }); + handleTitleOnlyChange(event: any) { + const titleOnly = event.target.checked; + this.updateUrl({ titleOnly, q: this.getQ() }); } handleTypeChange(i: Search, event: any) { @@ -1213,7 +1209,7 @@ export class Search extends Component { q, type, listingType, - postTitleOnly, + titleOnly, sort, communityId, creatorId, @@ -1227,7 +1223,7 @@ export class Search extends Component { q, type: type, listingType: listingType, - postTitleOnly: postTitleOnly?.toString(), + titleOnly: titleOnly?.toString(), communityId: communityId?.toString(), creatorId: creatorId?.toString(), page: page?.toString(), diff --git a/src/shared/markdown.ts b/src/shared/markdown.ts index 36e49d17..3a380af6 100644 --- a/src/shared/markdown.ts +++ b/src/shared/markdown.ts @@ -17,6 +17,8 @@ import markdown_it_highlightjs from "markdown-it-highlightjs/core"; import { Renderer, Token } from "markdown-it"; import { instanceLinkRegex, relTags } from "./config"; import { lazyHighlightjs } from "./lazy-highlightjs"; +import { HttpService } from "./services"; +import { WrappedLemmyHttp } from "./services/HttpService"; let Tribute: any; @@ -32,7 +34,7 @@ export const mdLimited: MarkdownIt = new MarkdownIt("zero").enable([ "strikethrough", ]); -export const customEmojis: EmojiMartCategory[] = []; +let customEmojis: EmojiMartCategory[] = []; export let customEmojisLookup: Map = new Map< string, @@ -204,14 +206,17 @@ export function setupMarkdown() { ) { //Provide custom renderer for our emojis to allow us to add a css class and force size dimensions on them. const item = tokens[idx] as any; - let title = item.attrs.length >= 3 ? item.attrs[2][1] : ""; + const url = item.attrs.length > 0 ? item.attrs[0][1] : ""; + const altText = item.attrs.length > 1 ? item.attrs[1][1] : ""; + const title = item.attrs.length > 2 ? item.attrs[2][1] : ""; const splitTitle = title.split(/ (.*)/, 2); const isEmoji = splitTitle[0] === "emoji"; + let shortcode: string | undefined; if (isEmoji) { - title = splitTitle[1]; + shortcode = splitTitle[1]; } - const customEmoji = customEmojisLookup.get(title); - const isLocalEmoji = customEmoji !== undefined; + // customEmojisLookup is empty in SSR, CSR rerenders markdown anyway + const isLocalEmoji = shortcode && customEmojisLookup.has(shortcode); if (!isLocalEmoji) { const imgElement = defaultImageRenderer?.(tokens, idx, options, env, self) ?? ""; @@ -222,10 +227,8 @@ export function setupMarkdown() { } else return ""; } return `${
-      customEmoji!.custom_emoji.alt_text
-    }`; + url + }" title="${shortcode}" alt="${altText}"/>`; }; md.renderer.rules.table_open = function () { return ''; @@ -247,12 +250,14 @@ export function setupMarkdown() { }; } -export function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) { +export function emojiMartCategories( + custom_emoji_views: CustomEmojiView[], +): EmojiMartCategory[] { const groupedEmojis = groupBy( custom_emoji_views, x => x.custom_emoji.category, ); - customEmojis.length = 0; + const customEmojis: EmojiMartCategory[] = []; for (const [category, emojis] of Object.entries(groupedEmojis)) { customEmojis.push({ id: category, @@ -265,63 +270,24 @@ export function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) { })), }); } + return customEmojis; +} + +export async function setupEmojiDataModel( + client: WrappedLemmyHttp = HttpService.client, +): Promise { + const emojisRes = await client.listCustomEmojis({ + ignore_page_limits: true, + }); + if (emojisRes.state !== "success") { + return false; + } + const custom_emoji_views = emojisRes.data.custom_emojis; + customEmojis = emojiMartCategories(custom_emoji_views); customEmojisLookup = new Map( custom_emoji_views.map(view => [view.custom_emoji.shortcode, view]), ); -} - -export function updateEmojiDataModel(custom_emoji_view: CustomEmojiView) { - const emoji: EmojiMartCustomEmoji = { - id: custom_emoji_view.custom_emoji.shortcode, - name: custom_emoji_view.custom_emoji.shortcode, - keywords: custom_emoji_view.keywords.map(x => x.keyword), - skins: [{ src: custom_emoji_view.custom_emoji.image_url }], - }; - const categoryIndex = customEmojis.findIndex( - x => x.id === custom_emoji_view.custom_emoji.category, - ); - if (categoryIndex === -1) { - customEmojis.push({ - id: custom_emoji_view.custom_emoji.category, - name: custom_emoji_view.custom_emoji.category, - emojis: [emoji], - }); - } else { - const emojiIndex = customEmojis[categoryIndex].emojis.findIndex( - x => x.id === custom_emoji_view.custom_emoji.shortcode, - ); - if (emojiIndex === -1) { - customEmojis[categoryIndex].emojis.push(emoji); - } else { - customEmojis[categoryIndex].emojis[emojiIndex] = emoji; - } - } - customEmojisLookup.set( - custom_emoji_view.custom_emoji.shortcode, - custom_emoji_view, - ); -} - -export function removeFromEmojiDataModel(id: number) { - let view: CustomEmojiView | undefined; - for (const item of customEmojisLookup.values()) { - if (item.custom_emoji.id === id) { - view = item; - break; - } - } - if (!view) return; - const categoryIndex = customEmojis.findIndex( - x => x.id === view?.custom_emoji.category, - ); - const emojiIndex = customEmojis[categoryIndex].emojis.findIndex( - x => x.id === view?.custom_emoji.shortcode, - ); - customEmojis[categoryIndex].emojis = customEmojis[ - categoryIndex - ].emojis.splice(emojiIndex, 1); - - customEmojisLookup.delete(view?.custom_emoji.shortcode); + return true; } export function getEmojiMart( @@ -329,9 +295,9 @@ export function getEmojiMart( customPickerOptions: any = {}, ) { const pickerOptions = { - ...customPickerOptions, onEmojiSelect: onEmojiSelect, custom: customEmojis, + ...customPickerOptions, }; return new Picker(pickerOptions); } @@ -416,7 +382,7 @@ export async function setupTribute() { }); } -interface EmojiMartCategory { +export interface EmojiMartCategory { id: string; name: string; emojis: EmojiMartCustomEmoji[]; diff --git a/src/shared/utils/app/comment-to-post-sort-type.ts b/src/shared/utils/app/comment-to-post-sort-type.ts new file mode 100644 index 00000000..8e9eced7 --- /dev/null +++ b/src/shared/utils/app/comment-to-post-sort-type.ts @@ -0,0 +1,21 @@ +import { CommentSortType, PostSortType } from "lemmy-js-client"; + +function assertType(_: T) {} + +export default function commentToPostSortType( + sort: CommentSortType, +): PostSortType { + switch (sort) { + case "Hot": + case "New": + case "Old": + case "Controversial": + return sort; + case "Top": + return "TopAll"; + default: { + assertType(sort); + return "Hot"; + } + } +} diff --git a/src/shared/utils/app/convert-comment-sort-type.ts b/src/shared/utils/app/convert-comment-sort-type.ts deleted file mode 100644 index 706671f5..00000000 --- a/src/shared/utils/app/convert-comment-sort-type.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { CommentSortType, SortType } from "lemmy-js-client"; - -export default function convertCommentSortType( - sort: SortType, -): CommentSortType { - switch (sort) { - case "TopAll": - case "TopHour": - case "TopSixHour": - case "TopTwelveHour": - case "TopDay": - case "TopWeek": - case "TopMonth": - case "TopThreeMonths": - case "TopSixMonths": - case "TopNineMonths": - case "TopYear": { - return "Top"; - } - case "New": { - return "New"; - } - case "Hot": - case "Active": { - return "Hot"; - } - default: { - return "Hot"; - } - } -} diff --git a/src/shared/utils/app/index.ts b/src/shared/utils/app/index.ts index 79aa812c..4ffeada1 100644 --- a/src/shared/utils/app/index.ts +++ b/src/shared/utils/app/index.ts @@ -1,11 +1,11 @@ import buildCommentsTree from "./build-comments-tree"; import { colorList } from "./color-list"; +import commentToPostSortType from "./comment-to-post-sort-type"; import commentsToFlatNodes from "./comments-to-flat-nodes"; import communityRSSUrl from "./community-rss-url"; import communitySearch from "./community-search"; import communitySelectName from "./community-select-name"; import communityToChoice from "./community-to-choice"; -import convertCommentSortType from "./convert-comment-sort-type"; import editComment from "./edit-comment"; import editCommentReply from "./edit-comment-reply"; import editCommentReport from "./edit-comment-report"; @@ -59,12 +59,12 @@ import isAnonymousPath from "./is-anonymous-path"; export { buildCommentsTree, colorList, + commentToPostSortType, commentsToFlatNodes, communityRSSUrl, communitySearch, communitySelectName, communityToChoice, - convertCommentSortType, editComment, editCommentReply, editCommentReport, diff --git a/src/shared/utils/app/initialize-site.ts b/src/shared/utils/app/initialize-site.ts index d6c6511e..7d50200b 100644 --- a/src/shared/utils/app/initialize-site.ts +++ b/src/shared/utils/app/initialize-site.ts @@ -1,11 +1,8 @@ import { GetSiteResponse } from "lemmy-js-client"; -import { setupEmojiDataModel, setupMarkdown } from "../../markdown"; +import { setupMarkdown } from "../../markdown"; import { UserService } from "../../services"; export default function initializeSite(site?: GetSiteResponse) { UserService.Instance.myUserInfo = site?.my_user; - if (site) { - setupEmojiDataModel(site.custom_emojis ?? []); - } setupMarkdown(); } diff --git a/src/shared/utils/app/post-to-comment-sort-type.ts b/src/shared/utils/app/post-to-comment-sort-type.ts index 0219eb98..53b01ca6 100644 --- a/src/shared/utils/app/post-to-comment-sort-type.ts +++ b/src/shared/utils/app/post-to-comment-sort-type.ts @@ -1,16 +1,39 @@ -import { CommentSortType, SortType } from "lemmy-js-client"; +import { CommentSortType, PostSortType } from "lemmy-js-client"; -export default function postToCommentSortType(sort: SortType): CommentSortType { +function assertType(_: T) {} + +export default function postToCommentSortType( + sort: PostSortType, +): CommentSortType { switch (sort) { - case "Active": case "Hot": - return "Hot"; case "New": - case "NewComments": - return "New"; case "Old": - return "Old"; - default: + case "Controversial": { + return sort; + } + case "TopAll": + case "TopHour": + case "TopSixHour": + case "TopTwelveHour": + case "TopDay": + case "TopWeek": + case "TopMonth": + case "TopThreeMonths": + case "TopSixMonths": + case "TopNineMonths": + case "TopYear": { return "Top"; + } + case "NewComments": + case "MostComments": + case "Scaled": + case "Active": { + return "Hot"; + } + default: { + assertType(sort); + return "Hot"; + } } }