From: Dessalines Date: Mon, 27 Mar 2023 16:49:46 +0000 (-0400) Subject: Merge branch 'custom-emojis' of https://github.com/makotech222/lemmy-ui into makotech... X-Git-Url: http://these/git/%7B%60%24%7BrepoUrl%7D/blob/master/docs/%7BrepoUrl%7D?a=commitdiff_plain;h=449957938009bd10339a0a38e954fe2eee119969;p=lemmy-ui.git Merge branch 'custom-emojis' of https://github.com/makotech222/lemmy-ui into makotech222-custom-emojis --- 449957938009bd10339a0a38e954fe2eee119969 diff --cc package.json index d73f3d0,fba9403..0e0426a --- a/package.json +++ b/package.json @@@ -54,9 -47,10 +56,10 @@@ "inferno-server": "^8.0.6", "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", - "lemmy-js-client": "0.17.2-rc.4", - "lemmy-js-client": "0.17.2-rc.3", ++ "lemmy-js-client": "0.17.2-rc.5", "markdown-it": "^13.0.1", "markdown-it-container": "^3.0.0", + "markdown-it-emoji": "^2.0.2", "markdown-it-footnote": "^3.0.3", "markdown-it-html5-embed": "^1.0.0", "markdown-it-sub": "^1.0.0", diff --cc src/shared/components/common/emoji-mart.tsx index 0000000,39b09bc..6210366 mode 000000,100644..100644 --- a/src/shared/components/common/emoji-mart.tsx +++ b/src/shared/components/common/emoji-mart.tsx @@@ -1,0 -1,33 +1,30 @@@ + import { Component } from "inferno"; + import { getEmojiMart } from "../../utils"; + - + interface EmojiMartProps { - onEmojiClick?(val: any): any; - pickerOptions: any; ++ onEmojiClick?(val: any): any; ++ pickerOptions: any; + } + -export class EmojiMart extends Component< - EmojiMartProps -> { - constructor(props: any, context: any) { - super(props, context); - this.handleEmojiClick = this.handleEmojiClick.bind(this); - } - componentDidMount() { - let div: any = document.getElementById("emoji-picker"); - if (div) { - div.appendChild(getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)); - } ++export class EmojiMart extends Component { ++ constructor(props: any, context: any) { ++ super(props, context); ++ this.handleEmojiClick = this.handleEmojiClick.bind(this); ++ } ++ componentDidMount() { ++ let div: any = document.getElementById("emoji-picker"); ++ if (div) { ++ div.appendChild( ++ getEmojiMart(this.handleEmojiClick, this.props.pickerOptions) ++ ); + } ++ } + - render() { - return ( -
- ); - } ++ render() { ++ return
; ++ } + - handleEmojiClick(e: any) { - this.props.onEmojiClick?.(e); - } ++ handleEmojiClick(e: any) { ++ this.props.onEmojiClick?.(e); ++ } + } diff --cc src/shared/components/common/emoji-picker.tsx index 0000000,cb46651..1149583 mode 000000,100644..100644 --- a/src/shared/components/common/emoji-picker.tsx +++ b/src/shared/components/common/emoji-picker.tsx @@@ -1,0 -1,62 +1,62 @@@ + import { Component, linkEvent } from "inferno"; + import { i18n } from "../../i18next"; + import { EmojiMart } from "./emoji-mart"; + import { Icon } from "./icon"; + + interface EmojiPickerProps { - onEmojiClick?(val: any): any; ++ onEmojiClick?(val: any): any; + } + + interface EmojiPickerState { - showPicker: boolean, ++ showPicker: boolean; + } + -export class EmojiPicker extends Component< - EmojiPickerProps, - EmojiPickerState -> { - private emptyState: EmojiPickerState = { - showPicker: false, - }; - state: EmojiPickerState; - constructor(props: any, context: any) { - super(props, context); - this.state = this.emptyState; - this.handleEmojiClick = this.handleEmojiClick.bind(this); - } - render() { - return ( - - ++export class EmojiPicker extends Component { ++ private emptyState: EmojiPickerState = { ++ showPicker: false, ++ }; ++ state: EmojiPickerState; ++ constructor(props: any, context: any) { ++ super(props, context); ++ this.state = this.emptyState; ++ this.handleEmojiClick = this.handleEmojiClick.bind(this); ++ } ++ render() { ++ return ( ++ ++ + - {this.state.showPicker && ( - <> -
- -
-
- - )} - - ); - } ++ {this.state.showPicker && ( ++ <> ++
++ ++
++
++ ++ )} ++ ++ ); ++ } + - togglePicker(i: EmojiPicker, e: any) { - e.preventDefault(); - i.setState({ showPicker: !i.state.showPicker }); - } ++ togglePicker(i: EmojiPicker, e: any) { ++ e.preventDefault(); ++ i.setState({ showPicker: !i.state.showPicker }); ++ } + - handleEmojiClick(e: any) { - this.props.onEmojiClick?.(e); - } -} ++ handleEmojiClick(e: any) { ++ this.props.onEmojiClick?.(e); ++ } ++} diff --cc src/shared/components/common/markdown-textarea.tsx index 001779e,07a966c..d7bb4c5 --- a/src/shared/components/common/markdown-textarea.tsx +++ b/src/shared/components/common/markdown-textarea.tsx @@@ -15,8 -17,8 +16,9 @@@ import setupTippy, setupTribute, toast, + uploadImage, } from "../../utils"; + import { EmojiPicker } from "./emoji-picker"; import { Icon, Spinner } from "./icon"; import { LanguageSelect } from "./language-select"; @@@ -226,6 -228,7 +228,9 @@@ export class MarkdownTextArea extends C > - this.handleEmoji(this,e)}> ++ this.handleEmoji(this, e)} ++ >
++ ); ++ } + - 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; - } ++ 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; ++ } + - getEditTooltip(cv: CustomEmojiViewForm) { - if (this.canEdit(cv)) - return i18n.t("save"); - else - return i18n.t("custom_emoji_save_validation"); - } ++ getEditTooltip(cv: CustomEmojiViewForm) { ++ if (this.canEdit(cv)) return i18n.t("save"); ++ else return i18n.t("custom_emoji_save_validation"); ++ } ++ ++ handlePageChange(page: number) { ++ this.setState({ page: page }); ++ } + - handlePageChange(page: number) { ++ 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(); ++ } + } ++ } + - 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() - } - } - } ++ handleEmojiCategoryChange( ++ props: { form: EmojiForm; index: number }, ++ event: any ++ ) { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ let item = { ++ ...props.form.state.customEmojis[pagedIndex], ++ category: event.target.value, ++ changed: true, ++ }; ++ custom_emojis[pagedIndex] = item; ++ props.form.setState({ customEmojis: custom_emojis }); ++ } + - handleEmojiCategoryChange(props: { form: EmojiForm, index: number }, event: any) { - let custom_emojis = [...props.form.state.customEmojis]; - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - let item = { - ... props.form.state.customEmojis[pagedIndex], - category: event.target.value, - changed: true, - } - custom_emojis[pagedIndex] = item; - props.form.setState({ customEmojis: custom_emojis }); - } ++ handleEmojiShortCodeChange( ++ props: { form: EmojiForm; index: number }, ++ event: any ++ ) { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ let item = { ++ ...props.form.state.customEmojis[pagedIndex], ++ shortcode: event.target.value, ++ changed: true, ++ }; ++ custom_emojis[pagedIndex] = item; ++ props.form.setState({ customEmojis: custom_emojis }); ++ } + - handleEmojiShortCodeChange(props: { form: EmojiForm, index: number }, event: any) { - let custom_emojis = [...props.form.state.customEmojis]; - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - let item = { - ... props.form.state.customEmojis[pagedIndex], - shortcode: event.target.value, - changed: true, - } - custom_emojis[pagedIndex] = item; - props.form.setState({ customEmojis: custom_emojis }); - } ++ handleEmojiImageUrlChange( ++ props: { form: EmojiForm; index: number; overrideValue: string | null }, ++ event: any ++ ) { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ let item = { ++ ...props.form.state.customEmojis[pagedIndex], ++ image_url: props.overrideValue ?? event.target.value, ++ changed: true, ++ }; ++ custom_emojis[pagedIndex] = item; ++ props.form.setState({ customEmojis: custom_emojis }); ++ } + - handleEmojiImageUrlChange(props: { form: EmojiForm, index: number, overrideValue: string | null }, event: any) { - let custom_emojis = [...props.form.state.customEmojis]; - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - let item = { - ... props.form.state.customEmojis[pagedIndex], - image_url: props.overrideValue ?? event.target.value, - changed: true, - } - custom_emojis[pagedIndex] = item; - props.form.setState({ customEmojis: custom_emojis }); - } ++ handleEmojiAltTextChange( ++ props: { form: EmojiForm; index: number }, ++ event: any ++ ) { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ let item = { ++ ...props.form.state.customEmojis[pagedIndex], ++ alt_text: event.target.value, ++ changed: true, ++ }; ++ custom_emojis[pagedIndex] = item; ++ props.form.setState({ customEmojis: custom_emojis }); ++ } + - handleEmojiAltTextChange(props: { form: EmojiForm, index: number }, event: any) { - let custom_emojis = [...props.form.state.customEmojis]; - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - let item = { - ... props.form.state.customEmojis[pagedIndex], - alt_text: event.target.value, - changed: true, - } - custom_emojis[pagedIndex] = item; - props.form.setState({ customEmojis: custom_emojis }); - } ++ handleEmojiKeywordChange( ++ props: { form: EmojiForm; index: number }, ++ event: any ++ ) { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ let item = { ++ ...props.form.state.customEmojis[pagedIndex], ++ keywords: event.target.value, ++ changed: true, ++ }; ++ custom_emojis[pagedIndex] = item; ++ props.form.setState({ customEmojis: custom_emojis }); ++ } + - handleEmojiKeywordChange(props: { form: EmojiForm, index: number }, event: any) { - let custom_emojis = [...props.form.state.customEmojis]; - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - let item = { - ... props.form.state.customEmojis[pagedIndex], - keywords: event.target.value, - changed: true, - } - custom_emojis[pagedIndex] = item; - props.form.setState({ customEmojis: custom_emojis }); ++ handleDeleteEmojiClick(props: { ++ form: EmojiForm; ++ index: number; ++ cv: CustomEmojiViewForm; ++ }) { ++ let pagedIndex = ++ (props.form.state.page - 1) * props.form.itemsPerPage + props.index; ++ if (props.cv.id != 0) { ++ const deleteForm: DeleteCustomEmoji = { ++ id: props.cv.id, ++ auth: myAuth() ?? "", ++ }; ++ WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm)); ++ } else { ++ let custom_emojis = [...props.form.state.customEmojis]; ++ custom_emojis.splice(pagedIndex, 1); ++ props.form.setState({ customEmojis: custom_emojis }); + } ++ } + - handleDeleteEmojiClick(props: { form: EmojiForm, index: number, cv: CustomEmojiViewForm }) { - let pagedIndex = ((props.form.state.page - 1) * props.form.itemsPerPage) + props.index; - if (props.cv.id != 0) { - const deleteForm: DeleteCustomEmoji = { - id: props.cv.id, - auth: myAuth() ?? "" - }; - WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm)); - } - else { - let custom_emojis = [...props.form.state.customEmojis]; - custom_emojis.splice(pagedIndex, 1); - props.form.setState({ customEmojis: custom_emojis }); - } ++ handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) { ++ const keywords = props.cv.keywords ++ .split(" ") ++ .filter(x => x.length > 0) as string[]; ++ const uniqueKeywords = Array.from(new Set(keywords)); ++ if (props.cv.id != 0) { ++ const editForm: EditCustomEmoji = { ++ id: props.cv.id, ++ category: props.cv.category, ++ image_url: props.cv.image_url, ++ alt_text: props.cv.alt_text, ++ keywords: uniqueKeywords, ++ auth: myAuth() ?? "", ++ }; ++ WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm)); ++ } else { ++ const createForm: CreateCustomEmoji = { ++ category: props.cv.category, ++ shortcode: props.cv.shortcode, ++ image_url: props.cv.image_url, ++ alt_text: props.cv.alt_text, ++ keywords: uniqueKeywords, ++ auth: myAuth() ?? "", ++ }; ++ WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm)); + } ++ } + - handleEditEmojiClick(props: { form: EmojiForm, cv: CustomEmojiViewForm }) { - const keywords = props.cv.keywords.split(" ").filter(x => x.length > 0) as string[]; - const uniqueKeywords = Array.from(new Set(keywords)); - if (props.cv.id != 0) { - const editForm: EditCustomEmoji = { - id: props.cv.id, - category: props.cv.category, - image_url: props.cv.image_url, - alt_text: props.cv.alt_text, - keywords: uniqueKeywords, - auth: myAuth() ?? "" - }; - WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm)); - } - else { - const createForm: CreateCustomEmoji = { - category: props.cv.category, - shortcode: props.cv.shortcode, - image_url: props.cv.image_url, - alt_text: props.cv.alt_text, - keywords: uniqueKeywords, - auth: myAuth() ?? "" - }; - WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm)); - } - } ++ handleAddEmojiClick(form: EmojiForm, event: any) { ++ event.preventDefault(); ++ let custom_emojis = [...form.state.customEmojis]; ++ const page = ++ 1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage); ++ let item: CustomEmojiViewForm = { ++ id: 0, ++ shortcode: "", ++ alt_text: "", ++ category: "", ++ image_url: "", ++ keywords: "", ++ changed: true, ++ page: page, ++ }; ++ custom_emojis.push(item); ++ form.setState({ customEmojis: custom_emojis, page: page }); ++ } + - handleAddEmojiClick(form: EmojiForm, event: any) { - event.preventDefault(); - let custom_emojis = [...form.state.customEmojis]; - const page = 1 + (Math.floor((form.state.customEmojis.length) / form.itemsPerPage)) - let item: CustomEmojiViewForm = { - id: 0, - shortcode: "", - alt_text: "", - category: "", - image_url: "", - keywords: "", - changed: true, - page: page - } - custom_emojis.push(item); - form.setState({ customEmojis: custom_emojis, page: page }); ++ handleImageUpload(props: { form: EmojiForm; index: number }, event: any) { ++ let file: any; ++ if (event.target) { ++ event.preventDefault(); ++ file = event.target.files[0]; ++ } else { ++ file = event; + } + - async handleImageUpload(props: { form: EmojiForm, index: number }, event: any) { - event.preventDefault(); - let file = event.target.files[0]; - const formData = new FormData(); - formData.append("images[]", file); - - props.form.setState({ loading: true }); - let res: any = await fetch(pictrsUri, { - method: "POST", - body: formData, - }); - let data = await res.json(); - props.form.setState({ loading: false }); - if (data.msg != "ok") { - toast(JSON.stringify(data), "danger"); - } - else { - let hash = data.files[0].file; - let url = `${pictrsUri}/${hash}`; - props.form.handleEmojiImageUrlChange({form: props.form, index: props.index, overrideValue: url}, event) ++ uploadImage(file) ++ .then(res => { ++ console.log("pictrs upload:"); ++ console.log(res); ++ if (res.msg === "ok") { ++ pictrsDeleteToast( ++ `${i18n.t("click_to_delete_picture")}: ${file.name}`, ++ `${i18n.t("picture_deleted")}: ${file.name}`, ++ `${i18n.t("failed_to_delete_picture")}: ${file.name}`, ++ res.delete_url as string ++ ); ++ } else { ++ toast(JSON.stringify(res), "danger"); ++ let hash = res.files?.at(0)?.file; ++ let url = `${res.url}/${hash}`; ++ props.form.handleEmojiImageUrlChange( ++ { form: props.form, index: props.index, overrideValue: url }, ++ event ++ ); + } - } ++ }) ++ .catch(error => { ++ console.error(error); ++ toast(error, "danger"); ++ }); ++ } + - configurePicker(): any { - return { - data: { categories: [], emojis: [], aliases: [] }, - maxFrequentRows: 0, - dynamicWidth: true, - }; - } ++ configurePicker(): any { ++ return { ++ data: { categories: [], emojis: [], aliases: [] }, ++ maxFrequentRows: 0, ++ dynamicWidth: true, ++ }; ++ } + - parseMessage(msg: any) { - let op = wsUserOp(msg); - console.log(msg); - if (msg.error) { - toast(i18n.t(msg.error), "danger"); - this.context.router.history.push("/"); - this.setState({ loading: false }); - return; - } - else if (op == UserOperation.CreateCustomEmoji) { - let data = wsJsonToRes(msg); - const custom_emoji_view = data.custom_emoji; - updateEmojiDataModel(custom_emoji_view) - let currentEmojis = this.state.customEmojis; - let newEmojiIndex = currentEmojis.findIndex(x => x.shortcode == custom_emoji_view.custom_emoji.shortcode) - currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id; - currentEmojis[newEmojiIndex].changed = false; - this.setState({ customEmojis: currentEmojis }); - toast(i18n.t("saved_emoji")); - this.setState({ loading: false }); - } - else if (op == UserOperation.EditCustomEmoji) { - let data = wsJsonToRes(msg); - const custom_emoji_view = data.custom_emoji; - updateEmojiDataModel(data.custom_emoji) - let currentEmojis = this.state.customEmojis; - let newEmojiIndex = currentEmojis.findIndex(x => x.shortcode == custom_emoji_view.custom_emoji.shortcode) - currentEmojis[newEmojiIndex].changed = false; - this.setState({ customEmojis: currentEmojis }); - toast(i18n.t("saved_emoji")); - this.setState({ loading: false }); - } - else if (op == UserOperation.DeleteCustomEmoji) { - let data = wsJsonToRes(msg); - if (data.success) { - removeFromEmojiDataModel(data.id); - let custom_emojis = [...this.state.customEmojis.filter(x => x.id != data.id)]; - this.setState({ customEmojis: custom_emojis }); - toast(i18n.t("deleted_emoji")); - } - this.setState({ loading: false }); - } ++ parseMessage(msg: any) { ++ let op = wsUserOp(msg); ++ console.log(msg); ++ if (msg.error) { ++ toast(i18n.t(msg.error), "danger"); ++ this.context.router.history.push("/"); ++ this.setState({ loading: false }); ++ return; ++ } else if (op == UserOperation.CreateCustomEmoji) { ++ let data = wsJsonToRes(msg); ++ const custom_emoji_view = data.custom_emoji; ++ updateEmojiDataModel(custom_emoji_view); ++ let currentEmojis = this.state.customEmojis; ++ let newEmojiIndex = currentEmojis.findIndex( ++ x => x.shortcode == custom_emoji_view.custom_emoji.shortcode ++ ); ++ currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id; ++ currentEmojis[newEmojiIndex].changed = false; ++ this.setState({ customEmojis: currentEmojis }); ++ toast(i18n.t("saved_emoji")); ++ this.setState({ loading: false }); ++ } else if (op == UserOperation.EditCustomEmoji) { ++ let data = wsJsonToRes(msg); ++ const custom_emoji_view = data.custom_emoji; ++ updateEmojiDataModel(data.custom_emoji); ++ let currentEmojis = this.state.customEmojis; ++ let newEmojiIndex = currentEmojis.findIndex( ++ x => x.shortcode == custom_emoji_view.custom_emoji.shortcode ++ ); ++ currentEmojis[newEmojiIndex].changed = false; ++ this.setState({ customEmojis: currentEmojis }); ++ toast(i18n.t("saved_emoji")); ++ this.setState({ loading: false }); ++ } else if (op == UserOperation.DeleteCustomEmoji) { ++ let data = wsJsonToRes(msg); ++ if (data.success) { ++ removeFromEmojiDataModel(data.id); ++ let custom_emojis = [ ++ ...this.state.customEmojis.filter(x => x.id != data.id), ++ ]; ++ this.setState({ customEmojis: custom_emojis }); ++ toast(i18n.t("deleted_emoji")); ++ } ++ this.setState({ loading: false }); + } ++ } + } - diff --cc src/shared/utils.ts index 79e2dd4,6e38565..3b73389 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@@ -1,4 -1,5 +1,5 @@@ ++import { Picker } from "emoji-mart"; import emojiShortName from "emoji-short-name"; -import { Picker } from 'emoji-mart' import { BlockCommunityResponse, BlockPersonResponse, @@@ -29,10 -30,11 +31,13 @@@ } from "lemmy-js-client"; import { default as MarkdownIt } from "markdown-it"; import markdown_it_container from "markdown-it-container"; ++import markdown_it_emoji from "markdown-it-emoji/bare"; import markdown_it_footnote from "markdown-it-footnote"; import markdown_it_html5_embed from "markdown-it-html5-embed"; import markdown_it_sub from "markdown-it-sub"; import markdown_it_sup from "markdown-it-sup"; -import markdown_it_emoji from 'markdown-it-emoji/bare'; ++import Renderer from "markdown-it/lib/renderer"; ++import Token from "markdown-it/lib/token"; import moment from "moment"; import { Subscription } from "rxjs"; import { delay, retryWhen, take } from "rxjs/operators"; @@@ -75,6 -79,9 +80,12 @@@ export const markdownFieldCharacterLimi export const relTags = "noopener nofollow"; + let customEmojis: EmojiMartCategory[] = []; -export let customEmojisLookup: Map = new Map(); ++export let customEmojisLookup: Map = new Map< ++ string, ++ CustomEmojiView ++>(); + const DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@@ -632,11 -622,20 +626,23 @@@ export function setupTribute() return `${item.original.val} ${shortName}`; }, selectTemplate: (item: any) => { - return `${item.original.val}`; - let customEmoji = customEmojisLookup.get(item.original.key)?.custom_emoji; - if (customEmoji == undefined) - return `${item.original.val}`; ++ let customEmoji = customEmojisLookup.get( ++ item.original.key ++ )?.custom_emoji; ++ if (customEmoji == undefined) return `${item.original.val}`; + else + return `![${customEmoji.alt_text}](${customEmoji.image_url} "${customEmoji.shortcode}")`; }, -- values: Object.entries(emojiShortName).map(e => { -- return { key: e[1], val: e[0] }; - }), - }).concat( - Array.from(customEmojisLookup.entries()).map((k) => ({ - key: k[0], - val: `${k[1].custom_emoji.alt_text}` - })) - ), ++ values: Object.entries(emojiShortName) ++ .map(e => { ++ return { key: e[1], val: e[0] }; ++ }) ++ .concat( ++ Array.from(customEmojisLookup.entries()).map(k => ({ ++ key: k[0], ++ val: `${k[1].custom_emoji.alt_text}`, ++ })) ++ ), allowSpaces: false, autocompleteMode: true, // TODO @@@ -680,6 -679,118 +686,143 @@@ }); } - - + function setupEmojiDataModel(custom_emoji_views: CustomEmojiView[]) { + let groupedEmojis = groupBy(custom_emoji_views, x => x.custom_emoji.category); + for (const [category, emojis] of Object.entries(groupedEmojis)) { + customEmojis.push({ + id: category, + name: category, - emojis: emojis.map(emoji => - ({ ++ emojis: emojis.map(emoji => ({ + id: emoji.custom_emoji.shortcode, + name: emoji.custom_emoji.shortcode, + keywords: emoji.keywords.map(x => x.keyword), - skins: [{ src: emoji.custom_emoji.image_url }] - })) - }) ++ skins: [{ src: emoji.custom_emoji.image_url }], ++ })), ++ }); + } - customEmojisLookup = new Map(custom_emoji_views.map(view => [view.custom_emoji.shortcode, view])); ++ 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 }] ++ skins: [{ src: custom_emoji_view.custom_emoji.image_url }], + }; - let categoryIndex = customEmojis.findIndex(x => x.id == custom_emoji_view.custom_emoji.category); ++ let 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 { - let emojiIndex = customEmojis[categoryIndex].emojis.findIndex(x => x.id == custom_emoji_view.custom_emoji.shortcode); ++ emojis: [emoji], ++ }); ++ } else { ++ let 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.push(emoji); ++ } else { + customEmojis[categoryIndex].emojis[emojiIndex] = emoji; + } + } - customEmojisLookup.set(custom_emoji_view.custom_emoji.shortcode,custom_emoji_view); ++ customEmojisLookup.set( ++ custom_emoji_view.custom_emoji.shortcode, ++ custom_emoji_view ++ ); + } + + export function removeFromEmojiDataModel(id: number) { + let view: CustomEmojiView | undefined; + for (let 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); ++ 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); + } + + function setupMarkdown() { + const markdownItConfig: MarkdownIt.Options = { + html: false, + linkify: true, + typographer: true, + }; + - const emojiDefs = Array.from(customEmojisLookup.entries()).reduce((main, [key, value]) => ({...main, [key]: value}), {}) ++ const emojiDefs = Array.from(customEmojisLookup.entries()).reduce( ++ (main, [key, value]) => ({ ...main, [key]: value }), ++ {} ++ ); + md = new MarkdownIt(markdownItConfig) + .use(markdown_it_sub) + .use(markdown_it_sup) + .use(markdown_it_footnote) + .use(markdown_it_html5_embed, html5EmbedConfig) + .use(markdown_it_container, "spoiler", spoilerConfig) + .use(markdown_it_emoji, { - defs: emojiDefs ++ defs: emojiDefs, + }); + + mdNoImages = new MarkdownIt(markdownItConfig) + .use(markdown_it_sub) + .use(markdown_it_sup) + .use(markdown_it_footnote) + .use(markdown_it_html5_embed, html5EmbedConfig) + .use(markdown_it_container, "spoiler", spoilerConfig) + .use(markdown_it_emoji, { - defs: emojiDefs ++ defs: emojiDefs, + }) + .disable("image"); + var defaultRenderer = md.renderer.rules.image; - md.renderer.rules.image = function (tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, self: Renderer) { ++ md.renderer.rules.image = function ( ++ tokens: Token[], ++ idx: number, ++ options: MarkdownIt.Options, ++ env: any, ++ self: Renderer ++ ) { + //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; + const title = item.attrs.length >= 3 ? item.attrs[2][1] : ""; + const src: string = item.attrs[0][1]; + const isCustomEmoji = customEmojisLookup.get(title) != undefined; + if (!isCustomEmoji) { + return defaultRenderer?.(tokens, idx, options, env, self) ?? ""; + } + const alt_text = item.content; + return `${alt_text}`; - } ++ }; + } + -export function getEmojiMart(onEmojiSelect: (e: any) => void, customPickerOptions: any = {}) { - const pickerOptions = { ...customPickerOptions, onEmojiSelect: onEmojiSelect, custom: customEmojis } ++export function getEmojiMart( ++ onEmojiSelect: (e: any) => void, ++ customPickerOptions: any = {} ++) { ++ const pickerOptions = { ++ ...customPickerOptions, ++ onEmojiSelect: onEmojiSelect, ++ custom: customEmojis, ++ }; + return new Picker(pickerOptions); + } + var tippyInstance: any; if (isBrowser()) { tippyInstance = tippy("[data-tippy-content]"); @@@ -1407,8 -1520,8 +1552,10 @@@ export function nsfwCheck return !nsfw || (nsfw && myShowNsfw); } - export function getRandomFromList(list?: T[]): T | undefined { - return list?.at(Math.floor(Math.random() * list.length)); + export function getRandomFromList(list: T[]): T | undefined { - return list.length == 0 ? undefined : list.at(Math.floor(Math.random() * list.length)); ++ return list.length == 0 ++ ? undefined ++ : list.at(Math.floor(Math.random() * list.length)); } /** @@@ -1442,9 -1555,25 +1589,35 @@@ export function selectableLanguages } } } + +export function uploadImage(image: File): Promise { + const client = new LemmyHttp(httpBase); + + return client.uploadImage({ image }); +} ++ + interface EmojiMartCategory { - id: string, - name: string, - emojis: EmojiMartCustomEmoji[] ++ id: string; ++ name: string; ++ emojis: EmojiMartCustomEmoji[]; + } + + interface EmojiMartCustomEmoji { - id: string, - name: string, - keywords: string[], - skins: EmojiMartSkin[] ++ id: string; ++ name: string; ++ keywords: string[]; ++ skins: EmojiMartSkin[]; + } + + interface EmojiMartSkin { - src: string ++ src: string; + } + -const groupBy = (array: T[], predicate: (value: T, index: number, array: T[]) => string) => ++const groupBy = ( ++ array: T[], ++ predicate: (value: T, index: number, array: T[]) => string ++) => + array.reduce((acc, value, index, array) => { + (acc[predicate(value, index, array)] ||= []).push(value); + return acc; - }, {} as { [key: string]: T[] }); ++ }, {} as { [key: string]: T[] }); diff --cc yarn.lock index 1502167,34eba62..b32721d --- a/yarn.lock +++ b/yarn.lock @@@ -5419,13 -5439,12 +5429,13 @@@ leac@^0.6.0 resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912" integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg== - lemmy-js-client@0.17.2-rc.4: - version "0.17.2-rc.4" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.4.tgz#b4a2d935e2a8d427c8e30ecaac77a46e02354363" - integrity sha512-pV9JALCUb7hvdP2my9ksWThuLciHWwg0MkUL5ClDfTl0ql5Xk+UY3FJ6NCpsOWErBjfLQvqoep/23W92ISh1+Q== -lemmy-js-client@0.17.2-rc.3: - version "0.17.2-rc.3" - resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.3.tgz#dc2a33e9228aef260b03a6e1f55698a2f975f979" - integrity sha512-FlWEPMrW2Q/FbtihLOHq2YtcRuoX7700LweCnsm6R6dD6SzsnWy9nKJhn24fcjcR2o6tw0oZKgP0ccq9jPDgfQ== ++lemmy-js-client@0.17.2-rc.5: ++ version "0.17.2-rc.5" ++ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.5.tgz#8dbfa01fc293d63d72d8294d5584d4e71c9c08be" ++ integrity sha512-B2VibqJvevVDiYK7yfMPZrx0GdC4XgpN2bgouzMgXZsn+HENALIAm5K+sZhD40/NCd69MglWTlYtFYg9d4YxOA== dependencies: - node-fetch "2.6.6" + cross-fetch "^3.1.5" + form-data "^4.0.0" levn@^0.4.1: version "0.4.1"