diff --git a/.dockerignore b/.dockerignore index 9d990ab9ce..fe87f6e600 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ .gitattributes .gitignore .github +.vscode public/system public/assets public/packs @@ -20,6 +21,7 @@ postgres14 redis elasticsearch chart +storybook-static .yarn/ !.yarn/patches !.yarn/plugins diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml index 191eb5bc2c..2889032197 100644 --- a/.github/workflows/crowdin-download-stable.yml +++ b/.github/workflows/crowdin-download-stable.yml @@ -51,7 +51,7 @@ jobs: # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.6 + uses: peter-evans/create-pull-request@v7.0.8 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' diff --git a/.nvmrc b/.nvmrc index 4a203c23d8..f1c8f6b0d0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.17 +22.18 diff --git a/Gemfile.lock b/Gemfile.lock index 19a97ef496..2b6ba0d485 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -287,7 +287,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.65.1) + haml_lint (0.66.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -607,7 +607,7 @@ GEM parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.6.0) + pg (1.6.1) pghero (3.7.0) activerecord (>= 7.1) playwright-ruby-client (1.54.0) @@ -721,7 +721,7 @@ GEM connection_pool redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.10.0) + regexp_parser (2.11.0) reline (0.6.2) io-console (~> 0.5) request_store (1.7.0) @@ -805,7 +805,7 @@ GEM ruby-prof (1.7.2) base64 ruby-progressbar (1.13.0) - ruby-saml (1.18.0) + ruby-saml (1.18.1) nokogiri (>= 1.13.10) rexml ruby-vips (2.2.4) diff --git a/app/javascript/flavours/glitch/api_types/accounts.ts b/app/javascript/flavours/glitch/api_types/accounts.ts index b93054a1f6..913a201fef 100644 --- a/app/javascript/flavours/glitch/api_types/accounts.ts +++ b/app/javascript/flavours/glitch/api_types/accounts.ts @@ -37,7 +37,7 @@ export interface BaseApiAccountJSON { roles?: ApiAccountJSON[]; statuses_count: number; uri: string; - url: string; + url?: string; username: string; moved?: ApiAccountJSON; suspended?: boolean; diff --git a/app/javascript/flavours/glitch/api_types/notifications.ts b/app/javascript/flavours/glitch/api_types/notifications.ts index d173083dbd..a54d2d3b50 100644 --- a/app/javascript/flavours/glitch/api_types/notifications.ts +++ b/app/javascript/flavours/glitch/api_types/notifications.ts @@ -13,6 +13,7 @@ export const allNotificationTypes = [ 'favourite', 'reblog', 'mention', + 'quote', 'poll', 'status', 'update', @@ -28,6 +29,7 @@ export type NotificationWithStatusType = | 'reblog' | 'status' | 'mention' + | 'quote' | 'poll' | 'update'; diff --git a/app/javascript/flavours/glitch/components/status_prepend.jsx b/app/javascript/flavours/glitch/components/status_prepend.jsx index 60dd0a4ce7..41589061da 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.jsx +++ b/app/javascript/flavours/glitch/components/status_prepend.jsx @@ -101,6 +101,14 @@ export default class StatusPrepend extends PureComponent { values={{ name: link }} /> ); + case 'quote': + return ( + + ); } return null; }; diff --git a/app/javascript/flavours/glitch/features/emoji/constants.ts b/app/javascript/flavours/glitch/features/emoji/constants.ts index a5ec9e6e2b..09022371b2 100644 --- a/app/javascript/flavours/glitch/features/emoji/constants.ts +++ b/app/javascript/flavours/glitch/features/emoji/constants.ts @@ -15,17 +15,6 @@ export const SKIN_TONE_CODES = [ 0x1f3ff, // Dark skin tone ] as const; -// TODO: Test and create fallback for browsers that do not handle the /v flag. -export const UNICODE_EMOJI_REGEX = /\p{RGI_Emoji}/v; -// See: https://www.unicode.org/reports/tr51/#valid-emoji-tag-sequences -export const UNICODE_FLAG_EMOJI_REGEX = - /\p{RGI_Emoji_Flag_Sequence}|\p{RGI_Emoji_Tag_Sequence}/v; -export const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; -export const ANY_EMOJI_REGEX = new RegExp( - `(${UNICODE_EMOJI_REGEX.source}|${CUSTOM_EMOJI_REGEX.source})`, - 'gv', -); - // Emoji rendering modes. A mode is what we are using to render emojis, a style is what the user has selected. export const EMOJI_MODE_NATIVE = 'native'; export const EMOJI_MODE_NATIVE_WITH_FLAGS = 'native-flags'; diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx b/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx index fdda62a3e6..b1939c4fda 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx +++ b/app/javascript/flavours/glitch/features/emoji/emoji_html.tsx @@ -1,5 +1,7 @@ import type { ComponentPropsWithoutRef, ElementType } from 'react'; +import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment'; + import { useEmojify } from './hooks'; import type { CustomEmojiMapArg } from './types'; @@ -12,7 +14,7 @@ type EmojiHTMLProps = Omit< as?: Element; }; -export const EmojiHTML = ({ +export const ModernEmojiHTML = ({ extraEmojis, htmlString, as: asElement, // Rename for syntax highlighting @@ -29,3 +31,18 @@ export const EmojiHTML = ({ ); }; + +export const EmojiHTML = ( + props: EmojiHTMLProps, +) => { + if (isModernEmojiEnabled()) { + return ; + } + const Wrapper = props.as ?? 'div'; + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/features/emoji/hooks.ts b/app/javascript/flavours/glitch/features/emoji/hooks.ts index c5c2773100..a985c3e97f 100644 --- a/app/javascript/flavours/glitch/features/emoji/hooks.ts +++ b/app/javascript/flavours/glitch/features/emoji/hooks.ts @@ -8,7 +8,6 @@ import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment'; import { toSupportedLocale } from './locale'; import { determineEmojiMode } from './mode'; -import { emojifyElement } from './render'; import type { CustomEmojiMapArg, EmojiAppState, @@ -39,6 +38,7 @@ export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) { async (input: string) => { const wrapper = document.createElement('div'); wrapper.innerHTML = input; + const { emojifyElement } = await import('./render'); const result = await emojifyElement(wrapper, appState, extra); if (result) { setEmojifiedText(result.innerHTML); diff --git a/app/javascript/flavours/glitch/features/emoji/render.ts b/app/javascript/flavours/glitch/features/emoji/render.ts index afc87b88df..d00c88ab4f 100644 --- a/app/javascript/flavours/glitch/features/emoji/render.ts +++ b/app/javascript/flavours/glitch/features/emoji/render.ts @@ -9,7 +9,6 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, EMOJI_STATE_MISSING, - ANY_EMOJI_REGEX, } from './constants'; import { searchCustomEmojisByShortcodes, @@ -32,7 +31,12 @@ import type { LocaleOrCustom, UnicodeEmojiToken, } from './types'; -import { emojiLogger, stringHasAnyEmoji, stringHasUnicodeFlags } from './utils'; +import { + anyEmojiRegex, + emojiLogger, + stringHasAnyEmoji, + stringHasUnicodeFlags, +} from './utils'; const log = emojiLogger('render'); @@ -207,7 +211,7 @@ export function tokenizeText(text: string): TokenizedText { const tokens = []; let lastIndex = 0; - for (const match of text.matchAll(ANY_EMOJI_REGEX)) { + for (const match of text.matchAll(anyEmojiRegex())) { if (match.index > lastIndex) { tokens.push(text.slice(lastIndex, match.index)); } diff --git a/app/javascript/flavours/glitch/features/emoji/utils.ts b/app/javascript/flavours/glitch/features/emoji/utils.ts index 89f8d92646..9cb177e4ad 100644 --- a/app/javascript/flavours/glitch/features/emoji/utils.ts +++ b/app/javascript/flavours/glitch/features/emoji/utils.ts @@ -1,23 +1,32 @@ import debug from 'debug'; -import { - CUSTOM_EMOJI_REGEX, - UNICODE_EMOJI_REGEX, - UNICODE_FLAG_EMOJI_REGEX, -} from './constants'; +import { emojiRegexPolyfill } from '@/flavours/glitch/polyfills'; export function emojiLogger(segment: string) { return debug(`emojis:${segment}`); } export function stringHasUnicodeEmoji(input: string): boolean { - return UNICODE_EMOJI_REGEX.test(input); + return new RegExp(EMOJI_REGEX, supportedFlags()).test(input); } export function stringHasUnicodeFlags(input: string): boolean { - return UNICODE_FLAG_EMOJI_REGEX.test(input); + if (supportsRegExpSets()) { + return new RegExp( + '\\p{RGI_Emoji_Flag_Sequence}|\\p{RGI_Emoji_Tag_Sequence}', + 'v', + ).test(input); + } + return new RegExp( + // First range is regional indicator symbols, + // Second is a black flag + 0-9|a-z tag chars + cancel tag. + // See: https://en.wikipedia.org/wiki/Regional_indicator_symbol + '(?:\uD83C[\uDDE6-\uDDFF]){2}|\uD83C\uDFF4(?:\uDB40[\uDC30-\uDC7A])+\uDB40\uDC7F', + ).test(input); } +// Constant as this is supported by all browsers. +const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; export function stringHasCustomEmoji(input: string) { return CUSTOM_EMOJI_REGEX.test(input); } @@ -25,3 +34,23 @@ export function stringHasCustomEmoji(input: string) { export function stringHasAnyEmoji(input: string) { return stringHasUnicodeEmoji(input) || stringHasCustomEmoji(input); } + +export function anyEmojiRegex() { + return new RegExp( + `${EMOJI_REGEX}|${CUSTOM_EMOJI_REGEX.source}`, + supportedFlags('gi'), + ); +} + +function supportsRegExpSets() { + return 'unicodeSets' in RegExp.prototype; +} + +function supportedFlags(flags = '') { + if (supportsRegExpSets()) { + return `${flags}v`; + } + return flags; +} + +const EMOJI_REGEX = emojiRegexPolyfill?.source ?? '\\p{RGI_Emoji}'; diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index cebb589691..a57a7993da 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -8,8 +8,8 @@ import { withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; - import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; +import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import { Account } from 'flavours/glitch/components/account'; @@ -229,6 +229,31 @@ class Notification extends ImmutablePureComponent { ); } + renderQuote (notification) { + return ( +