Merge pull request #3255 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 4896d2c4c6d3bd6b878c5a075b6611c65d4203b2
This commit is contained in:
commit
8f00874a0e
@ -318,21 +318,3 @@ MAX_POLL_OPTION_CHARS=100
|
|||||||
# -----------------------
|
# -----------------------
|
||||||
IP_RETENTION_PERIOD=31556952
|
IP_RETENTION_PERIOD=31556952
|
||||||
SESSION_RETENTION_PERIOD=31556952
|
SESSION_RETENTION_PERIOD=31556952
|
||||||
|
|
||||||
# Fetch All Replies Behavior
|
|
||||||
# --------------------------
|
|
||||||
|
|
||||||
# Period to wait between fetching replies (in minutes)
|
|
||||||
FETCH_REPLIES_COOLDOWN_MINUTES=15
|
|
||||||
|
|
||||||
# Period to wait after a post is first created before fetching its replies (in minutes)
|
|
||||||
FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
|
|
||||||
|
|
||||||
# Max number of replies to fetch - total, recursively through a whole reply tree
|
|
||||||
FETCH_REPLIES_MAX_GLOBAL=1000
|
|
||||||
|
|
||||||
# Max number of replies to fetch - for a single post
|
|
||||||
FETCH_REPLIES_MAX_SINGLE=500
|
|
||||||
|
|
||||||
# Max number of replies Collection pages to fetch - total
|
|
||||||
FETCH_REPLIES_MAX_PAGES=500
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@
|
|||||||
/public/packs
|
/public/packs
|
||||||
/public/packs-dev
|
/public/packs-dev
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
|
stats.html
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
33
Gemfile.lock
33
Gemfile.lock
@ -90,7 +90,7 @@ GEM
|
|||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
android_key_attestation (0.3.0)
|
android_key_attestation (0.3.0)
|
||||||
annotaterb (4.19.0)
|
annotaterb (4.20.0)
|
||||||
activerecord (>= 6.0.0)
|
activerecord (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
@ -116,7 +116,7 @@ GEM
|
|||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
benchmark (0.4.1)
|
benchmark (0.5.0)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
@ -168,7 +168,7 @@ GEM
|
|||||||
cose (1.3.1)
|
cose (1.3.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 1.0)
|
openssl-signature_algorithm (~> 1.0)
|
||||||
crack (1.0.0)
|
crack (1.0.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@ -190,10 +190,10 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.2.0)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.0, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.0, < 8.2)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
@ -224,7 +224,7 @@ GEM
|
|||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erb (5.0.2)
|
erb (5.1.1)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.4.0)
|
et-orbi (1.4.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
@ -426,7 +426,8 @@ GEM
|
|||||||
loofah (2.24.1)
|
loofah (2.24.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
@ -442,7 +443,7 @@ GEM
|
|||||||
mime-types-data (3.2025.0924)
|
mime-types-data (3.2025.0924)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.5)
|
minitest (5.26.0)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.17.0)
|
multi_json (1.17.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
@ -705,9 +706,9 @@ GEM
|
|||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.7.0)
|
request_store (1.7.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.2.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.6.1)
|
rouge (4.6.1)
|
||||||
@ -821,9 +822,9 @@ GEM
|
|||||||
thor (>= 1.0, < 3.0)
|
thor (>= 1.0, < 3.0)
|
||||||
simple-navigation (4.4.0)
|
simple-navigation (4.4.0)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (5.3.1)
|
simple_form (5.4.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
activemodel (>= 5.2)
|
activemodel (>= 7.0)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
@ -910,7 +911,7 @@ GEM
|
|||||||
activesupport
|
activesupport
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
webmock (3.25.1)
|
webmock (3.26.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
|||||||
@ -159,7 +159,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_quoted_status
|
def set_quoted_status
|
||||||
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
@quoted_status = Status.find(status_params[:quoted_status_id])&.proper if status_params[:quoted_status_id].present?
|
||||||
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
# TODO: distinguish between non-existing and non-quotable posts
|
# TODO: distinguish between non-existing and non-quotable posts
|
||||||
|
|||||||
@ -70,7 +70,7 @@ function loaded() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll('.emojify').forEach((content) => {
|
document.querySelectorAll('.emojify').forEach((content) => {
|
||||||
content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system.
|
content.innerHTML = emojify(content.innerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
|
|||||||
@ -197,9 +197,14 @@ export function directCompose(account) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback ComposeSuccessCallback
|
||||||
|
* @param {Object} status
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {null | string} overridePrivacy
|
* @param {null | string} overridePrivacy
|
||||||
* @param {undefined | Function} successCallback
|
* @param {undefined | ComposeSuccessCallback} successCallback
|
||||||
*/
|
*/
|
||||||
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
@ -653,6 +658,7 @@ export function fetchComposeSuggestions(token) {
|
|||||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
case '#':
|
case '#':
|
||||||
|
case '#':
|
||||||
fetchComposeSuggestionsTags(dispatch, getState, token);
|
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -694,11 +700,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
} else if (suggestion.type === 'hashtag') {
|
} else if (suggestion.type === 'hashtag') {
|
||||||
completion = `#${suggestion.name}`;
|
completion = suggestion.name.slice(token.length - 1);
|
||||||
startPosition = position - 1;
|
startPosition = position + token.length;
|
||||||
} else if (suggestion.type === 'account') {
|
} else if (suggestion.type === 'account') {
|
||||||
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
|
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
|
||||||
startPosition = position;
|
startPosition = position - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
|
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
|
||||||
import { autoHideCW } from '../../utils/content_warning';
|
import { autoHideCW } from '../../utils/content_warning';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
@ -80,11 +77,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
} else {
|
} else {
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = normalStatus.content;
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
|
||||||
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
||||||
|
|
||||||
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
@ -120,14 +116,12 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStatusTranslation(translation, status) {
|
export function normalizeStatusTranslation(translation, status) {
|
||||||
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
|
|
||||||
|
|
||||||
const normalTranslation = {
|
const normalTranslation = {
|
||||||
detected_source_language: translation.detected_source_language,
|
detected_source_language: translation.detected_source_language,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
provider: translation.provider,
|
provider: translation.provider,
|
||||||
contentHtml: emojify(translation.content, emojiMap),
|
contentHtml: translation.content,
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
|
||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,9 +135,8 @@ export function normalizeStatusTranslation(translation, status) {
|
|||||||
|
|
||||||
export function normalizeAnnouncement(announcement) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = normalAnnouncement.content;
|
||||||
|
|
||||||
return normalAnnouncement;
|
return normalAnnouncement;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,20 @@ import {
|
|||||||
const randomUpTo = max =>
|
const randomUpTo = max =>
|
||||||
Math.floor(Math.random() * Math.floor(max));
|
Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('flavours/glitch/store').AppDispatch} Dispatch
|
||||||
|
* @typedef {import('flavours/glitch/store').GetState} GetState
|
||||||
|
* @typedef {import('redux').UnknownAction} UnknownAction
|
||||||
|
* @typedef {function(Dispatch, GetState): Promise<void>} FallbackFunction
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} timelineId
|
* @param {string} timelineId
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function, Function): Promise<void>} [options.fallback]
|
* @param {FallbackFunction} [options.fallback]
|
||||||
* @param {function(): void} [options.fillGaps]
|
* @param {function(): UnknownAction} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
@ -46,13 +53,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
|
||||||
return connectStream(channelName, params, (dispatch, getState) => {
|
return connectStream(channelName, params, (dispatch, getState) => {
|
||||||
|
// @ts-ignore
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
let pollingId;
|
let pollingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function(Function, Function): Promise<void>} fallback
|
* @param {FallbackFunction} fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const useFallback = async fallback => {
|
const useFallback = async fallback => {
|
||||||
@ -132,7 +140,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} dispatch
|
* @param {Dispatch} dispatch
|
||||||
*/
|
*/
|
||||||
async function refreshHomeTimelineAndNotification(dispatch) {
|
async function refreshHomeTimelineAndNotification(dispatch) {
|
||||||
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
||||||
@ -151,7 +159,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
connectTimelineStream('home', 'user', {}, {
|
||||||
|
fallback: refreshHomeTimelineAndNotification,
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: fillHomeTimelineGaps
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -159,7 +171,10 @@ export const connectUserStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -169,7 +184,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
||||||
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
|
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly })
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} columnId
|
* @param {string} columnId
|
||||||
@ -192,4 +210,7 @@ export const connectDirectStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectListStream = listId =>
|
export const connectListStream = listId =>
|
||||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillListTimelineGaps(listId)
|
||||||
|
});
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
|
||||||
|
|
||||||
import { useAppSelector } from '../store';
|
import { useAppSelector } from '../store';
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { useElementHandledLink } from './status/handled_link';
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
@ -21,22 +16,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
accountId,
|
accountId,
|
||||||
showDropdown = false,
|
showDropdown = false,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useLinks(showDropdown);
|
|
||||||
const handleNodeChange = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (
|
|
||||||
!showDropdown ||
|
|
||||||
!node ||
|
|
||||||
node.childNodes.length === 0 ||
|
|
||||||
isModernEmojiEnabled()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addDropdownToHashtags(node, accountId);
|
|
||||||
},
|
|
||||||
[showDropdown, accountId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: showDropdown ? accountId : undefined,
|
hashtagAccountId: showDropdown ? accountId : undefined,
|
||||||
});
|
});
|
||||||
@ -62,30 +41,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
htmlString={note}
|
htmlString={note}
|
||||||
extraEmojis={extraEmojis}
|
extraEmojis={extraEmojis}
|
||||||
className={classNames(className, 'translate')}
|
className={classNames(className, 'translate')}
|
||||||
onClickCapture={handleClick}
|
|
||||||
ref={handleNodeChange}
|
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const childNode of node.childNodes) {
|
|
||||||
if (!(childNode instanceof HTMLElement)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
childNode instanceof HTMLAnchorElement &&
|
|
||||||
(childNode.classList.contains('hashtag') ||
|
|
||||||
childNode.innerText.startsWith('#')) &&
|
|
||||||
!childNode.dataset.menuHashtag
|
|
||||||
) {
|
|
||||||
childNode.dataset.menuHashtag = accountId;
|
|
||||||
} else if (childNode.childNodes.length > 0) {
|
|
||||||
addDropdownToHashtags(childNode, accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
searchTokens: ['@', ':', '#'],
|
searchTokens: ['@', '@', ':', '#', '#'],
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || ['@', '@', ':', '#', '#'].indexOf(word[0]) === -1) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameWithoutDomain: FC<
|
export const DisplayNameWithoutDomain: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, className, children, localDomain: _, ...props }) => {
|
||||||
> = ({ account, className, children, ...props }) => {
|
|
||||||
return (
|
return (
|
||||||
<AnimateEmojiProvider
|
<AnimateEmojiProvider
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -5,9 +5,8 @@ import { EmojiHTML } from '../emoji/html';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameSimple: FC<
|
export const DisplayNameSimple: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, localDomain: _, ...props }) => {
|
||||||
> = ({ account, ...props }) => {
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { cleanExtraEmojis } from '@/flavours/glitch/features/emoji/normalize';
|
import { cleanExtraEmojis } from '@/flavours/glitch/features/emoji/normalize';
|
||||||
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
const parentContext = useContext(AnimateEmojiContext);
|
const parentContext = useContext(AnimateEmojiContext);
|
||||||
if (parentContext !== null) {
|
if (parentContext !== null) {
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper {...props} className={className} ref={ref}>
|
||||||
{...props}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(className, 'animate-parent')}
|
className={className}
|
||||||
onMouseEnter={handleEnter}
|
onMouseEnter={handleEnter}
|
||||||
onMouseLeave={handleLeave}
|
onMouseLeave={handleLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import type { CustomEmojiMapArg } from '@/flavours/glitch/features/emoji/types';
|
import type { CustomEmojiMapArg } from '@/flavours/glitch/features/emoji/types';
|
||||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
|
||||||
import type {
|
import type {
|
||||||
OnAttributeHandler,
|
OnAttributeHandler,
|
||||||
OnElementHandler,
|
OnElementHandler,
|
||||||
@ -22,7 +19,7 @@ export interface EmojiHTMLProps {
|
|||||||
onAttribute?: OnAttributeHandler;
|
onAttribute?: OnAttributeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
extraEmojis,
|
extraEmojis,
|
||||||
@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ModernEmojiHTML.displayName = 'ModernEmojiHTML';
|
EmojiHTML.displayName = 'EmojiHTML';
|
||||||
|
|
||||||
export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|
||||||
(props, ref) => {
|
|
||||||
const {
|
|
||||||
as: asElement,
|
|
||||||
htmlString,
|
|
||||||
extraEmojis,
|
|
||||||
className,
|
|
||||||
onElement,
|
|
||||||
onAttribute,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
const Wrapper = asElement ?? 'div';
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
{...rest}
|
|
||||||
ref={ref}
|
|
||||||
dangerouslySetInnerHTML={{ __html: htmlString }}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
|
|
||||||
|
|
||||||
export const EmojiHTML = isModernEmojiEnabled()
|
|
||||||
? ModernEmojiHTML
|
|
||||||
: LegacyEmojiHTML;
|
|
||||||
|
|||||||
@ -23,8 +23,6 @@ import { domain } from 'flavours/glitch/initial_state';
|
|||||||
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { useLinks } from '../hooks/useLinks';
|
|
||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId?: string }
|
{ accountId?: string }
|
||||||
@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef<
|
|||||||
!isMutual &&
|
!isMutual &&
|
||||||
!isFollower;
|
!isFollower;
|
||||||
|
|
||||||
const handleClick = useLinks();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -114,7 +110,7 @@ export const HoverCardAccount = forwardRef<
|
|||||||
className='hover-card__bio'
|
className='hover-card__bio'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='account-fields' onClickCapture={handleClick}>
|
<div className='account-fields'>
|
||||||
<AccountFields
|
<AccountFields
|
||||||
fields={account.fields.take(2)}
|
fields={account.fields.take(2)}
|
||||||
emojis={account.emojis}
|
emojis={account.emojis}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { OnElementHandler } from '@/flavours/glitch/utils/html';
|
|||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
import type { EmojiHTMLProps } from '../emoji/html';
|
import type { EmojiHTMLProps } from '../emoji/html';
|
||||||
import { ModernEmojiHTML } from '../emoji/html';
|
import { EmojiHTML } from '../emoji/html';
|
||||||
import { useElementHandledLink } from '../status/handled_link';
|
import { useElementHandledLink } from '../status/handled_link';
|
||||||
|
|
||||||
export const HTMLBlock = polymorphicForwardRef<
|
export const HTMLBlock = polymorphicForwardRef<
|
||||||
@ -25,6 +25,6 @@ export const HTMLBlock = polymorphicForwardRef<
|
|||||||
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
||||||
[onLinkElement, onParentElement],
|
[onLinkElement, onParentElement],
|
||||||
);
|
);
|
||||||
return <ModernEmojiHTML {...props} onElement={onElement} />;
|
return <EmojiHTML {...props} onElement={onElement} />;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
|
import { fetchPoll, vote } from 'flavours/glitch/actions/polls';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
|
||||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
|
|
||||||
import type * as Model from 'flavours/glitch/models/poll';
|
import type * as Model from 'flavours/glitch/models/poll';
|
||||||
import type { Status } from 'flavours/glitch/models/status';
|
import type { Status } from 'flavours/glitch/models/status';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
@ -235,12 +233,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
|
|||||||
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
||||||
|
|
||||||
if (!titleHtml) {
|
if (!titleHtml) {
|
||||||
const emojiMap = makeEmojiMap(poll.emojis);
|
titleHtml = escapeTextContentForBrowser(title);
|
||||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return titleHtml;
|
return titleHtml;
|
||||||
}, [option, poll, title]);
|
}, [option, title]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleOptionChange = useCallback(() => {
|
const handleOptionChange = useCallback(() => {
|
||||||
|
|||||||
@ -750,8 +750,6 @@ class Status extends ImmutablePureComponent {
|
|||||||
collapsible
|
collapsible
|
||||||
media={media}
|
media={media}
|
||||||
onCollapsedToggle={this.handleCollapsedToggle}
|
onCollapsedToggle={this.handleCollapsedToggle}
|
||||||
tagLinks={settings.get('tag_misleading_links')}
|
|
||||||
rewriteMentions={settings.get('rewrite_mentions')}
|
|
||||||
{...statusContentProps}
|
{...statusContentProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -132,7 +132,12 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
}, [tagLinks]);
|
}, [tagLinks]);
|
||||||
|
|
||||||
// Handle hashtags
|
// Handle hashtags
|
||||||
if (text.startsWith('#') || prevText?.endsWith('#')) {
|
if (
|
||||||
|
text.startsWith('#') ||
|
||||||
|
prevText?.endsWith('#') ||
|
||||||
|
text.startsWith('#') ||
|
||||||
|
prevText?.endsWith('#')
|
||||||
|
) {
|
||||||
const hashtag = text.slice(1).trim();
|
const hashtag = text.slice(1).trim();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -15,10 +15,8 @@ import { Poll } from 'flavours/glitch/components/poll';
|
|||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
import { languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { HandledLink, isLinkMisleading, tagMisleadingLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
|
||||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
|
||||||
@ -72,6 +70,17 @@ const mapStateToProps = state => ({
|
|||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const compareUrls = (href1, href2) => {
|
||||||
|
try {
|
||||||
|
const url1 = new URL(href1);
|
||||||
|
const url2 = new URL(href2);
|
||||||
|
|
||||||
|
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
@ -81,8 +90,6 @@ class StatusContent extends PureComponent {
|
|||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
collapsible: PropTypes.bool,
|
collapsible: PropTypes.bool,
|
||||||
onCollapsedToggle: PropTypes.func,
|
onCollapsedToggle: PropTypes.func,
|
||||||
tagLinks: PropTypes.bool,
|
|
||||||
rewriteMentions: PropTypes.string,
|
|
||||||
languages: ImmutablePropTypes.map,
|
languages: ImmutablePropTypes.map,
|
||||||
intl: PropTypes.object,
|
intl: PropTypes.object,
|
||||||
// from react-router
|
// from react-router
|
||||||
@ -91,14 +98,8 @@ class StatusContent extends PureComponent {
|
|||||||
history: PropTypes.object.isRequired
|
history: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
tagLinks: true,
|
|
||||||
rewriteMentions: 'no',
|
|
||||||
};
|
|
||||||
|
|
||||||
_updateStatusLinks () {
|
_updateStatusLinks () {
|
||||||
const node = this.node;
|
const node = this.node;
|
||||||
const { tagLinks, rewriteMentions } = this.props;
|
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return;
|
return;
|
||||||
@ -116,51 +117,6 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if modern emoji is enabled, as it handles links using the HandledLink component.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
let link, mention;
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', `@${mention.get('acct')}`);
|
|
||||||
link.setAttribute('data-hover-card-account', mention.get('id'));
|
|
||||||
if (rewriteMentions !== 'no') {
|
|
||||||
while (link.firstChild) link.removeChild(link.firstChild);
|
|
||||||
link.appendChild(document.createTextNode('@'));
|
|
||||||
const acctSpan = document.createElement('span');
|
|
||||||
acctSpan.textContent = rewriteMentions === 'acct' ? mention.get('acct') : mention.get('username');
|
|
||||||
link.appendChild(acctSpan);
|
|
||||||
}
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
link.setAttribute('rel', 'noopener nofollow');
|
|
||||||
|
|
||||||
if (tagLinks) tagMisleadingLink(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@ -171,22 +127,6 @@ class StatusContent extends PureComponent {
|
|||||||
this._updateStatusLinks();
|
this._updateStatusLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown = (e) => {
|
handleMouseDown = (e) => {
|
||||||
this.startXY = [e.clientX, e.clientY];
|
this.startXY = [e.clientX, e.clientY];
|
||||||
};
|
};
|
||||||
@ -224,7 +164,7 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
handleElement = (element, { key, ...props }, children) => {
|
handleElement = (element, { key, ...props }, children) => {
|
||||||
if (element instanceof HTMLAnchorElement) {
|
if (element instanceof HTMLAnchorElement) {
|
||||||
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
|
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
|
||||||
return (
|
return (
|
||||||
<HandledLink
|
<HandledLink
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -1,30 +1,10 @@
|
|||||||
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
import type { OnAttributeHandler } from '../utils/html';
|
import type { OnAttributeHandler } from '../utils/html';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
|
||||||
|
|
||||||
const stripRelMe = (html: string) => {
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
|
||||||
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
|
||||||
link.rel = link.rel
|
|
||||||
.split(' ')
|
|
||||||
.filter((x: string) => x !== 'me')
|
|
||||||
.join(' ');
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
return body?.innerHTML ?? '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||||
if (name === 'rel' && tagName === 'a') {
|
if (name === 'rel' && tagName === 'a') {
|
||||||
if (value === 'me') {
|
if (value === 'me') {
|
||||||
@ -47,10 +27,6 @@ interface Props {
|
|||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||||
<EmojiHTML
|
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
|
||||||
as='span'
|
|
||||||
htmlString={stripRelMe(link)}
|
|
||||||
onAttribute={onAttribute}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -70,7 +70,7 @@ function loaded() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll('.emojify').forEach((content) => {
|
document.querySelectorAll('.emojify').forEach((content) => {
|
||||||
content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system.
|
content.innerHTML = emojify(content.innerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
|
|||||||
@ -47,7 +47,6 @@ import { IconButton } from 'flavours/glitch/components/icon_button';
|
|||||||
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
|
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
|
||||||
import { DomainPill } from 'flavours/glitch/features/account/components/domain_pill';
|
import { DomainPill } from 'flavours/glitch/features/account/components/domain_pill';
|
||||||
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
|
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
|
||||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
|
||||||
import { useIdentity } from 'flavours/glitch/identity_context';
|
import { useIdentity } from 'flavours/glitch/identity_context';
|
||||||
import {
|
import {
|
||||||
autoPlayGif,
|
autoPlayGif,
|
||||||
@ -202,7 +201,6 @@ export const AccountHeader: React.FC<{
|
|||||||
state.relationships.get(accountId),
|
state.relationships.get(accountId),
|
||||||
);
|
);
|
||||||
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
||||||
const handleLinkClick = useLinks();
|
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
const handleBlock = useCallback(() => {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@ -856,10 +854,7 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
{!(suspended || hidden) && (
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div
|
<div className='account__header__bio'>
|
||||||
className='account__header__bio'
|
|
||||||
onClickCapture={handleLinkClick}
|
|
||||||
>
|
|
||||||
{account.id !== me && signedIn && (
|
{account.id !== me && signedIn && (
|
||||||
<AccountNote accountId={accountId} />
|
<AccountNote accountId={accountId} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import Overlay from 'react-overlays/Overlay';
|
|||||||
|
|
||||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
import { useSystemEmojiFont } from 'flavours/glitch/initial_state';
|
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
@ -99,12 +98,12 @@ class ModifierPickerMenu extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||||
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' size={22} skin={1} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' size={22} skin={1} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' size={22} skin={2} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' size={22} skin={2} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' size={22} skin={3} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' size={22} skin={3} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' size={22} skin={4} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' size={22} skin={4} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' size={22} skin={5} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' size={22} skin={5} /></button>
|
||||||
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' size={22} skin={6} native={useSystemEmojiFont} /></button>
|
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' size={22} skin={6} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -134,12 +133,12 @@ class ModifierPicker extends PureComponent {
|
|||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { active, modifier } = this.props;
|
const { active, modifier } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='emoji-picker-dropdown__modifiers'>
|
<div className='emoji-picker-dropdown__modifiers'>
|
||||||
<Emoji emoji='fist' size={22} skin={modifier} onClick={this.handleClick} native={useSystemEmojiFont} />
|
<Emoji emoji='fist' size={22} skin={modifier} onClick={this.handleClick} />
|
||||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -291,7 +290,6 @@ class EmojiPickerMenuImpl extends PureComponent {
|
|||||||
notFound={notFoundFn}
|
notFound={notFoundFn}
|
||||||
autoFocus={this.state.readyToFocus}
|
autoFocus={this.state.readyToFocus}
|
||||||
emojiTooltip
|
emojiTooltip
|
||||||
native={useSystemEmojiFont}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModifierPicker
|
<ModifierPicker
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
import { assetHost } from 'flavours/glitch/utils/config';
|
||||||
|
|
||||||
import { autoPlayGif, useSystemEmojiFont } from '../../initial_state';
|
import { autoPlayGif } from '../../initial_state';
|
||||||
|
|
||||||
import { unicodeMapping } from './emoji_unicode_mapping_light';
|
import { unicodeMapping } from './emoji_unicode_mapping_light';
|
||||||
|
|
||||||
@ -40,13 +39,13 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
let unicode_emoji;
|
let unicode_emoji;
|
||||||
|
|
||||||
// Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode:)
|
// Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode:
|
||||||
if (customEmojis === null) {
|
if (customEmojis === null) {
|
||||||
while (i < str.length && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
|
while (i < str.length && !(unicode_emoji = trie.search(str.slice(i)))) {
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
|
while (i < str.length && str[i] !== ':' && !(unicode_emoji = trie.search(str.slice(i)))) {
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => {
|
|||||||
* Legacy emoji processing function.
|
* Legacy emoji processing function.
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {object} customEmojis
|
* @param {object} customEmojis
|
||||||
* @param {boolean} force If true, always emojify even if modern emoji is enabled
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
const emojify = (str, customEmojis = {}, force = false) => {
|
const emojify = (str, customEmojis = {}) => {
|
||||||
if (isModernEmojiEnabled() && !force) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = str;
|
wrapper.innerHTML = str;
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
|||||||
|
|
||||||
import data from './emoji_data.json';
|
import data from './emoji_data.json';
|
||||||
import emojiMap from './emoji_map.json';
|
import emojiMap from './emoji_map.json';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
|
||||||
|
|
||||||
emojiMartUncompress(data);
|
emojiMartUncompress(data);
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type {
|
|||||||
ShortCodesToEmojiData,
|
ShortCodesToEmojiData,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_utils';
|
||||||
|
|
||||||
type Emojis = Record<
|
type Emojis = Record<
|
||||||
NonNullable<keyof ShortCodesToEmojiData>,
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
@ -23,7 +23,7 @@ type Emojis = Record<
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
skins,
|
_skins,
|
||||||
categories,
|
categories,
|
||||||
short_names,
|
short_names,
|
||||||
_emojisWithoutShortCodes,
|
_emojisWithoutShortCodes,
|
||||||
@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export { emojis, skins, categories, short_names };
|
export { emojis, categories, short_names };
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// This code is largely borrowed from:
|
// This code is largely borrowed from:
|
||||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
||||||
|
|
||||||
import * as data from './emoji_mart_data_light';
|
import { emojis, categories } from './emoji_mart_data_light';
|
||||||
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
||||||
|
|
||||||
let originalPool = {};
|
let originalPool = {};
|
||||||
@ -10,8 +10,8 @@ let emojisList = {};
|
|||||||
let emoticonsList = {};
|
let emoticonsList = {};
|
||||||
let customEmojisList = [];
|
let customEmojisList = [];
|
||||||
|
|
||||||
for (let emoji in data.emojis) {
|
for (let emoji in emojis) {
|
||||||
let emojiData = data.emojis[emoji];
|
let emojiData = emojis[emoji];
|
||||||
let { short_names, emoticons } = emojiData;
|
let { short_names, emoticons } = emojiData;
|
||||||
let id = short_names[0];
|
let id = short_names[0];
|
||||||
|
|
||||||
@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
if (include.length || exclude.length) {
|
if (include.length || exclude.length) {
|
||||||
pool = {};
|
pool = {};
|
||||||
|
|
||||||
data.categories.forEach(category => {
|
categories.forEach(category => {
|
||||||
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
||||||
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
||||||
if (!isIncluded || isExcluded) {
|
if (!isIncluded || isExcluded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
|
category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (custom.length) {
|
if (custom.length) {
|
||||||
@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
if (emojisToShowFilter) {
|
if (emojisToShowFilter) {
|
||||||
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
|
results = results.filter((result) => emojisToShowFilter(emojis[result.id]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results && results.length > maxResults) {
|
if (results && results.length > maxResults) {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart';
|
|||||||
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
import { assetHost } from 'flavours/glitch/utils/config';
|
||||||
|
|
||||||
import { EMOJI_MODE_NATIVE } from './constants';
|
import { EMOJI_MODE_NATIVE } from './constants';
|
||||||
@ -27,7 +26,7 @@ const Emoji = ({
|
|||||||
sheetSize={sheetSize}
|
sheetSize={sheetSize}
|
||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -51,7 +50,7 @@ const Picker = ({
|
|||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type {
|
|||||||
ShortCodesToEmojiDataKey,
|
ShortCodesToEmojiDataKey,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_utils';
|
||||||
|
|
||||||
type UnicodeMapping = Record<
|
type UnicodeMapping = Record<
|
||||||
FilenameData[number][0],
|
FilenameData[number][0],
|
||||||
|
|||||||
@ -209,50 +209,9 @@ function intersect(a, b) {
|
|||||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepMerge(a, b) {
|
|
||||||
let o = {};
|
|
||||||
|
|
||||||
for (let key in a) {
|
|
||||||
let originalValue = a[key],
|
|
||||||
value = originalValue;
|
|
||||||
|
|
||||||
if (Object.hasOwn(b, key)) {
|
|
||||||
value = b[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = deepMerge(originalValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
o[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/sonicdoe/measure-scrollbar
|
|
||||||
function measureScrollbar() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
|
|
||||||
div.style.width = '100px';
|
|
||||||
div.style.height = '100px';
|
|
||||||
div.style.overflow = 'scroll';
|
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.top = '-9999px';
|
|
||||||
|
|
||||||
document.body.appendChild(div);
|
|
||||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
|
||||||
document.body.removeChild(div);
|
|
||||||
|
|
||||||
return scrollbarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getData,
|
getData,
|
||||||
getSanitizedData,
|
getSanitizedData,
|
||||||
uniq,
|
uniq,
|
||||||
intersect,
|
intersect,
|
||||||
deepMerge,
|
|
||||||
unifiedToNative,
|
|
||||||
measureScrollbar,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import { autoPlayGif } from '@/flavours/glitch/initial_state';
|
|
||||||
|
|
||||||
const PARENT_MAX_DEPTH = 10;
|
|
||||||
|
|
||||||
export function handleAnimateGif(event: MouseEvent) {
|
|
||||||
// We already check this in ui/index.jsx, but just to be sure.
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { target, type } = event;
|
|
||||||
const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate.
|
|
||||||
|
|
||||||
if (target instanceof HTMLImageElement) {
|
|
||||||
setAnimateGif(target, animate);
|
|
||||||
} else if (!(target instanceof HTMLElement) || target === document.body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent: HTMLElement | null = null;
|
|
||||||
let iter = 0;
|
|
||||||
|
|
||||||
if (target.classList.contains('animate-parent')) {
|
|
||||||
parent = target;
|
|
||||||
} else {
|
|
||||||
// Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'.
|
|
||||||
let current: HTMLElement | null = target;
|
|
||||||
while (current) {
|
|
||||||
if (iter >= PARENT_MAX_DEPTH) {
|
|
||||||
return; // We can just exit right now.
|
|
||||||
}
|
|
||||||
current = current.parentElement;
|
|
||||||
if (current?.classList.contains('animate-parent')) {
|
|
||||||
parent = current;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
iter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Affect all animated children within the parent.
|
|
||||||
if (parent) {
|
|
||||||
const animatedChildren =
|
|
||||||
parent.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
|
||||||
for (const child of animatedChildren) {
|
|
||||||
setAnimateGif(child, animate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAnimateGif(image: HTMLImageElement, animate: boolean) {
|
|
||||||
const { classList, dataset } = image;
|
|
||||||
if (
|
|
||||||
!classList.contains('custom-emoji') ||
|
|
||||||
!dataset.static ||
|
|
||||||
!dataset.original
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
image.src = animate ? dataset.original : dataset.static;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { initialState } from '@/flavours/glitch/initial_state';
|
import { initialState } from '@/flavours/glitch/initial_state';
|
||||||
import { loadWorker } from '@/flavours/glitch/utils/workers';
|
|
||||||
|
|
||||||
import { toSupportedLocale } from './locale';
|
import { toSupportedLocale } from './locale';
|
||||||
import { emojiLogger } from './utils';
|
import { emojiLogger } from './utils';
|
||||||
|
// eslint-disable-next-line import/default -- Importing via worker loader.
|
||||||
|
import EmojiWorker from './worker?worker&inline';
|
||||||
|
|
||||||
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
||||||
|
|
||||||
@ -16,9 +17,7 @@ export function initializeEmoji() {
|
|||||||
log('initializing emojis');
|
log('initializing emojis');
|
||||||
if (!worker && 'Worker' in window) {
|
if (!worker && 'Worker' in window) {
|
||||||
try {
|
try {
|
||||||
worker = loadWorker(new URL('./worker', import.meta.url), {
|
worker = new EmojiWorker();
|
||||||
type: 'module',
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Error creating web worker:', err);
|
console.warn('Error creating web worker:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
// taken from:
|
|
||||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
|
||||||
export const unicodeToFilename = (str) => {
|
|
||||||
let result = '';
|
|
||||||
let charCode = 0;
|
|
||||||
let p = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (i < str.length) {
|
|
||||||
charCode = str.charCodeAt(i++);
|
|
||||||
if (p) {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
|
|
||||||
p = 0;
|
|
||||||
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
|
|
||||||
p = charCode;
|
|
||||||
} else {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += charCode.toString(16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
function padLeft(str, num) {
|
|
||||||
while (str.length < num) {
|
|
||||||
str = '0' + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const unicodeToUnifiedName = (str) => {
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
|
||||||
if (i > 0) {
|
|
||||||
output += '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
// taken from:
|
||||||
|
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
||||||
|
export function unicodeToFilename(str: string) {
|
||||||
|
let result = '';
|
||||||
|
let charCode = 0;
|
||||||
|
let p = 0;
|
||||||
|
let i = 0;
|
||||||
|
while (i < str.length) {
|
||||||
|
charCode = str.charCodeAt(i++);
|
||||||
|
if (p) {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString(
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
p = 0;
|
||||||
|
} else if (0xd800 <= charCode && charCode <= 0xdbff) {
|
||||||
|
p = charCode;
|
||||||
|
} else {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += charCode.toString(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unicodeToUnifiedName(str: string) {
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
output += '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
output +=
|
||||||
|
str.codePointAt(i)?.toString(16).toUpperCase().padStart(4, '0') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@ -25,6 +25,14 @@ import StatusListContainer from '../ui/containers/status_list_container';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
|
title_local: {
|
||||||
|
id: 'column.firehose_local',
|
||||||
|
defaultMessage: 'Live feed for this server',
|
||||||
|
},
|
||||||
|
title_singular: {
|
||||||
|
id: 'column.firehose_singular',
|
||||||
|
defaultMessage: 'Live feed',
|
||||||
|
},
|
||||||
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,13 +195,23 @@ const Firehose = ({ feedType, multiColumn }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
if (canViewFeed(signedIn, permissions, localLiveFeedAccess) && canViewFeed(signedIn, permissions, remoteLiveFeedAccess)) {
|
||||||
|
title = messages.title;
|
||||||
|
} else if (canViewFeed(signedIn, permissions, localLiveFeedAccess)) {
|
||||||
|
title = messages.title_local;
|
||||||
|
} else {
|
||||||
|
title = messages.title_singular;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
icon='globe'
|
icon='globe'
|
||||||
iconComponent={PublicIcon}
|
iconComponent={PublicIcon}
|
||||||
active={hasUnread}
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(title)}
|
||||||
onPin={handlePin}
|
onPin={handlePin}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
|
|||||||
@ -1,444 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent, useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { animated, useTransition } from '@react-spring/web';
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
|
||||||
|
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
|
||||||
import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container';
|
|
||||||
import { unicodeMapping } from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light';
|
|
||||||
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/glitch/initial_state';
|
|
||||||
import { assetHost } from 'flavours/glitch/utils/config';
|
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
|
||||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class ContentWithRouter extends ImmutablePureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateLinks () {
|
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
let link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', mention.get('acct'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
} else {
|
|
||||||
let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
|
|
||||||
if (status) {
|
|
||||||
link.addEventListener('click', this.onStatusClick.bind(this, status), false);
|
|
||||||
}
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
link.setAttribute('rel', 'noopener');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onStatusClick = (status, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='announcements__item__content translate animate-parent'
|
|
||||||
ref={this.setRef}
|
|
||||||
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const Content = withRouter(ContentWithRouter);
|
|
||||||
|
|
||||||
class Emoji extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
emoji: PropTypes.string.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
hovered: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { emoji, emojiMap, hovered } = this.props;
|
|
||||||
|
|
||||||
if (unicodeMapping[emoji]) {
|
|
||||||
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione'
|
|
||||||
alt={emoji}
|
|
||||||
title={title}
|
|
||||||
src={`${assetHost}/emoji/${filename}.svg`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (emojiMap.get(emoji)) {
|
|
||||||
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
|
|
||||||
const shortCode = `:${emoji}:`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione custom-emoji'
|
|
||||||
alt={shortCode}
|
|
||||||
title={shortCode}
|
|
||||||
src={filename}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reaction extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reaction: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hovered: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
const { reaction, announcementId, addReaction, removeReaction } = this.props;
|
|
||||||
|
|
||||||
if (reaction.get('me')) {
|
|
||||||
removeReaction(announcementId, reaction.get('name'));
|
|
||||||
} else {
|
|
||||||
addReaction(announcementId, reaction.get('name'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = () => this.setState({ hovered: true });
|
|
||||||
|
|
||||||
handleMouseLeave = () => this.setState({ hovered: false });
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { reaction } = this.props;
|
|
||||||
|
|
||||||
let shortCode = reaction.get('name');
|
|
||||||
|
|
||||||
if (unicodeMapping[shortCode]) {
|
|
||||||
shortCode = unicodeMapping[shortCode].shortCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<animated.button
|
|
||||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
title={`:${shortCode}:`}
|
|
||||||
style={this.props.style}
|
|
||||||
// This does not use animate-parent as this component is directly rendered by React.
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
>
|
|
||||||
<span className='reactions-bar__item__emoji'>
|
|
||||||
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
|
|
||||||
</span>
|
|
||||||
<span className='reactions-bar__item__count'>
|
|
||||||
<AnimatedNumber value={reaction.get('count')} />
|
|
||||||
</span>
|
|
||||||
</animated.button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactionsBar = ({
|
|
||||||
announcementId,
|
|
||||||
reactions,
|
|
||||||
emojiMap,
|
|
||||||
addReaction,
|
|
||||||
removeReaction,
|
|
||||||
}) => {
|
|
||||||
const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
|
|
||||||
|
|
||||||
const handleEmojiPick = useCallback((emoji) => {
|
|
||||||
addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
|
|
||||||
}, [addReaction, announcementId]);
|
|
||||||
|
|
||||||
const transitions = useTransition(visibleReactions, {
|
|
||||||
from: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
enter: {
|
|
||||||
scale: 1,
|
|
||||||
},
|
|
||||||
leave: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
keys: visibleReactions.map(x => x.get('name')),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('reactions-bar', {
|
|
||||||
'reactions-bar--empty': visibleReactions.length === 0
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{transitions(({ scale }, reaction) => (
|
|
||||||
<Reaction
|
|
||||||
key={reaction.get('name')}
|
|
||||||
reaction={reaction}
|
|
||||||
style={{ transform: scale.to((s) => `scale(${s})`) }}
|
|
||||||
addReaction={addReaction}
|
|
||||||
removeReaction={removeReaction}
|
|
||||||
announcementId={announcementId}
|
|
||||||
emojiMap={emojiMap}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{visibleReactions.length < 8 && (
|
|
||||||
<EmojiPickerDropdown
|
|
||||||
onPickEmoji={handleEmojiPick}
|
|
||||||
button={<Icon id='plus' icon={AddIcon} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ReactionsBar.propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class Announcement extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
selected: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
unread: !this.props.announcement.get('read'),
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
const { selected, announcement } = this.props;
|
|
||||||
if (!selected && this.state.unread !== !announcement.get('read')) {
|
|
||||||
this.setState({ unread: !announcement.get('read') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
const { unread } = this.state;
|
|
||||||
const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
|
|
||||||
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
|
|
||||||
const now = new Date();
|
|
||||||
const hasTimeRange = startsAt && endsAt;
|
|
||||||
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
|
|
||||||
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
|
|
||||||
const skipTime = announcement.get('all_day');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements__item'>
|
|
||||||
<strong className='announcements__item__range'>
|
|
||||||
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
|
|
||||||
{hasTimeRange && <span> · <FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
|
|
||||||
</strong>
|
|
||||||
|
|
||||||
<Content announcement={announcement} />
|
|
||||||
|
|
||||||
<ReactionsBar
|
|
||||||
reactions={announcement.get('reactions')}
|
|
||||||
announcementId={announcement.get('id')}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{unread && <span className='announcements__item__unread' />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Announcements extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcements: ImmutablePropTypes.list,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
dismissAnnouncement: PropTypes.func.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.announcements.size > 0 && state.index >= props.announcements.size) {
|
|
||||||
return { index: props.announcements.size - 1 };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
_markAnnouncementAsRead () {
|
|
||||||
const { dismissAnnouncement, announcements } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
const announcement = announcements.get(announcements.size - 1 - index);
|
|
||||||
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeIndex = index => {
|
|
||||||
this.setState({ index: index % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNextClick = () => {
|
|
||||||
this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrevClick = () => {
|
|
||||||
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcements, intl } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
|
|
||||||
if (announcements.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements'>
|
|
||||||
<img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
|
|
||||||
|
|
||||||
<div className='announcements__container'>
|
|
||||||
<ReactSwipeableViews animateHeight animateTransitions={!reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
|
|
||||||
{announcements.map((announcement, idx) => (
|
|
||||||
<Announcement
|
|
||||||
key={announcement.get('id')}
|
|
||||||
announcement={announcement}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
intl={intl}
|
|
||||||
selected={index === idx}
|
|
||||||
disabled={disableSwiping}
|
|
||||||
/>
|
|
||||||
)).reverse()}
|
|
||||||
</ReactSwipeableViews>
|
|
||||||
|
|
||||||
{announcements.size > 1 && (
|
|
||||||
<div className='announcements__pagination'>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' iconComponent={ChevronLeftIcon} onClick={this.handlePrevClick} size={13} />
|
|
||||||
<span>{index + 1} / {announcements.size}</span>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' iconComponent={ChevronRightIcon} onClick={this.handleNextClick} size={13} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(Announcements);
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
|
||||||
import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements';
|
|
||||||
|
|
||||||
import Announcements from '../components/announcements';
|
|
||||||
|
|
||||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
announcements: state.getIn(['announcements', 'items']),
|
|
||||||
emojiMap: customEmojiMap(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
|
|
||||||
addReaction: (id, name) => dispatch(addReaction(id, name)),
|
|
||||||
removeReaction: (id, name) => dispatch(removeReaction(id, name)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
|
|
||||||
@ -9,10 +9,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
|
|||||||
|
|
||||||
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
||||||
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||||
import LegacyAnnouncements from '@/flavours/glitch/features/getting_started/containers/announcements_container';
|
|
||||||
import { mascot, reduceMotion } from '@/flavours/glitch/initial_state';
|
import { mascot, reduceMotion } from '@/flavours/glitch/initial_state';
|
||||||
import { createAppSelector, useAppSelector } from '@/flavours/glitch/store';
|
import { createAppSelector, useAppSelector } from '@/flavours/glitch/store';
|
||||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
@ -32,7 +30,7 @@ const announcementSelector = createAppSelector(
|
|||||||
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ModernAnnouncements: FC = () => {
|
export const Announcements: FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const announcements = useAppSelector(announcementSelector);
|
const announcements = useAppSelector(announcementSelector);
|
||||||
@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Announcements = isModernEmojiEnabled()
|
|
||||||
? ModernAnnouncements
|
|
||||||
: LegacyAnnouncements;
|
|
||||||
|
|||||||
@ -66,6 +66,10 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Trending' },
|
explore: { id: 'explore.title', defaultMessage: 'Trending' },
|
||||||
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
|
firehose_singular: {
|
||||||
|
id: 'column.firehose_singular',
|
||||||
|
defaultMessage: 'Live feed',
|
||||||
|
},
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
@ -299,7 +303,12 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
|||||||
icon='globe'
|
icon='globe'
|
||||||
iconComponent={PublicIcon}
|
iconComponent={PublicIcon}
|
||||||
isActive={isFirehoseActive}
|
isActive={isFirehoseActive}
|
||||||
text={intl.formatMessage(messages.firehose)}
|
text={intl.formatMessage(
|
||||||
|
canViewFeed(signedIn, permissions, localLiveFeedAccess) &&
|
||||||
|
canViewFeed(signedIn, permissions, remoteLiveFeedAccess)
|
||||||
|
? messages.firehose
|
||||||
|
: messages.firehose_singular,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,17 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
import type { History } from 'history';
|
|
||||||
|
|
||||||
import type { ApiMentionJSON } from '@/flavours/glitch/api_types/statuses';
|
|
||||||
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
||||||
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
import { useElementHandledLink } from '@/flavours/glitch/components/status/handled_link';
|
||||||
import type { Status } from '@/flavours/glitch/models/status';
|
import type { Status } from '@/flavours/glitch/models/status';
|
||||||
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
|
||||||
|
|
||||||
import type { Mention } from './embedded_status';
|
import type { Mention } from './embedded_status';
|
||||||
|
|
||||||
const handleMentionClick = (
|
|
||||||
history: History,
|
|
||||||
mention: ApiMentionJSON,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/@${mention.acct}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHashtagClick = (
|
|
||||||
history: History,
|
|
||||||
hashtag: string,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmbeddedStatusContent: React.FC<{
|
export const EmbeddedStatusContent: React.FC<{
|
||||||
status: Status;
|
status: Status;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ status, className }) => {
|
}> = ({ status, className }) => {
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const mentions = useMemo(
|
const mentions = useMemo(
|
||||||
() => (status.get('mentions') as List<Mention>).toJS(),
|
() => (status.get('mentions') as List<Mention>).toJS(),
|
||||||
[status],
|
[status],
|
||||||
@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{
|
|||||||
hrefToMention,
|
hrefToMention,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleContentRef = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (!node || isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll<HTMLAnchorElement>('a');
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
const mention = mentions.find((item) => link.href === item.url);
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleMentionClick.bind(null, history, mention),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('title', `@${mention.acct}`);
|
|
||||||
link.setAttribute('href', `/@${mention.acct}`);
|
|
||||||
} else if (
|
|
||||||
link.textContent.startsWith('#') ||
|
|
||||||
link.previousSibling?.textContent?.endsWith('#')
|
|
||||||
) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleHashtagClick.bind(null, history, link.text),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[mentions, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
className={className}
|
className={className}
|
||||||
ref={handleContentRef}
|
|
||||||
lang={status.get('language') as string}
|
lang={status.get('language') as string}
|
||||||
htmlString={status.get('contentHtml') as string}
|
htmlString={status.get('contentHtml') as string}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -79,13 +79,6 @@ export const DetailedStatus: React.FC<{
|
|||||||
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
||||||
const nodeRef = useRef<HTMLDivElement>();
|
const nodeRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const rewriteMentions = useAppSelector(
|
|
||||||
(state) => state.local_settings.get('rewrite_mentions', false) as boolean,
|
|
||||||
);
|
|
||||||
const tagMisleadingLinks = useAppSelector(
|
|
||||||
(state) =>
|
|
||||||
state.local_settings.get('tag_misleading_links', false) as boolean,
|
|
||||||
);
|
|
||||||
const letterboxMedia = useAppSelector(
|
const letterboxMedia = useAppSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.local_settings.getIn(['media', 'letterbox'], false) as boolean,
|
state.local_settings.getIn(['media', 'letterbox'], false) as boolean,
|
||||||
@ -442,8 +435,6 @@ export const DetailedStatus: React.FC<{
|
|||||||
<StatusContent
|
<StatusContent
|
||||||
status={status}
|
status={status}
|
||||||
onTranslate={handleTranslate}
|
onTranslate={handleTranslate}
|
||||||
tagLinks={tagMisleadingLinks}
|
|
||||||
rewriteMentions={rewriteMentions}
|
|
||||||
{...(statusContentProps as any)}
|
{...(statusContentProps as any)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import { IconButton } from 'flavours/glitch/components/icon_button';
|
|||||||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||||
import MediaAttachments from 'flavours/glitch/components/media_attachments';
|
import MediaAttachments from 'flavours/glitch/components/media_attachments';
|
||||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
|
||||||
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
|
||||||
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
import { CustomEmojiProvider } from '@/flavours/glitch/components/emoji/context';
|
||||||
|
|
||||||
@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
const { index, versions, language, onClose } = this.props;
|
const { index, versions, language, onClose } = this.props;
|
||||||
const currentVersion = versions.get(index);
|
const currentVersion = versions.get(index);
|
||||||
|
|
||||||
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
|
const content = currentVersion.get('content');
|
||||||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text'));
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const content = emojify(currentVersion.get('content'), emojiMap);
|
|
||||||
const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap);
|
|
||||||
|
|
||||||
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
||||||
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
||||||
@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
as="span"
|
as="span"
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
htmlString={emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)}
|
htmlString={escapeTextContentForBrowser(option.get('title'))}
|
||||||
lang={language}
|
lang={language}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -25,12 +25,11 @@ import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
|||||||
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
import { handleAnimateGif } from '../emoji/handlers';
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, autoPlayGif } from '../../initial_state';
|
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@ -395,11 +394,6 @@ class UI extends PureComponent {
|
|||||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
|
||||||
if (!autoPlayGif) {
|
|
||||||
window.addEventListener('mouseover', handleAnimateGif, { passive: true });
|
|
||||||
window.addEventListener('mouseout', handleAnimateGif, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
document.addEventListener('drop', this.handleDrop, false);
|
document.addEventListener('drop', this.handleDrop, false);
|
||||||
@ -458,8 +452,6 @@ class UI extends PureComponent {
|
|||||||
|
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
window.removeEventListener('mouseover', handleAnimateGif);
|
|
||||||
window.removeEventListener('mouseout', handleAnimateGif);
|
|
||||||
|
|
||||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { openURL } from 'flavours/glitch/actions/search';
|
|
||||||
import { useAppDispatch } from 'flavours/glitch/store';
|
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.classList.contains('mention') &&
|
|
||||||
!element.classList.contains('hashtag');
|
|
||||||
|
|
||||||
const isHashtagClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.textContent.startsWith('#') ||
|
|
||||||
element.previousSibling?.textContent?.endsWith('#');
|
|
||||||
|
|
||||||
export const useLinks = (skipHashtags?: boolean) => {
|
|
||||||
const history = useHistory();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const handleHashtagClick = useCallback(
|
|
||||||
(element: HTMLAnchorElement) => {
|
|
||||||
const { textContent } = element;
|
|
||||||
|
|
||||||
if (!textContent) return;
|
|
||||||
|
|
||||||
history.push(`/tags/${textContent.replace(/^#/, '')}`);
|
|
||||||
},
|
|
||||||
[history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMentionClick = useCallback(
|
|
||||||
async (element: HTMLAnchorElement) => {
|
|
||||||
const result = await dispatch(openURL({ url: element.href }));
|
|
||||||
|
|
||||||
if (isFulfilled(result)) {
|
|
||||||
if (result.payload.accounts[0]) {
|
|
||||||
history.push(`/@${result.payload.accounts[0].acct}`);
|
|
||||||
} else if (result.payload.statuses[0]) {
|
|
||||||
history.push(
|
|
||||||
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
} else if (isRejected(result)) {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(e: React.MouseEvent) => {
|
|
||||||
// Exit early if modern emoji is enabled, as this is handled by HandledLink.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = (e.target as HTMLElement).closest('a');
|
|
||||||
|
|
||||||
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMentionClick(target)) {
|
|
||||||
e.preventDefault();
|
|
||||||
void handleMentionClick(target);
|
|
||||||
} else if (isHashtagClick(target) && !skipHashtags) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleHashtagClick(target);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[skipHashtags, handleMentionClick, handleHashtagClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
return handleClick;
|
|
||||||
};
|
|
||||||
@ -49,7 +49,6 @@ interface InitialStateMeta {
|
|||||||
status_page_url: string;
|
status_page_url: string;
|
||||||
terms_of_service_enabled: boolean;
|
terms_of_service_enabled: boolean;
|
||||||
emoji_style?: string;
|
emoji_style?: string;
|
||||||
system_emoji_font?: boolean;
|
|
||||||
default_content_type: string;
|
default_content_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +176,6 @@ export const maxFeedHashtags = initialState?.max_feed_hashtags ?? 4;
|
|||||||
export const favouriteModal = getMeta('favourite_modal');
|
export const favouriteModal = getMeta('favourite_modal');
|
||||||
export const pollLimits = initialState?.poll_limits;
|
export const pollLimits = initialState?.poll_limits;
|
||||||
export const defaultContentType = getMeta('default_content_type');
|
export const defaultContentType = getMeta('default_content_type');
|
||||||
export const useSystemEmojiFont = getMeta('system_emoji_font');
|
|
||||||
|
|
||||||
export function getAccessToken(): string | undefined {
|
export function getAccessToken(): string | undefined {
|
||||||
return getMeta('access_token');
|
return getMeta('access_token');
|
||||||
|
|||||||
@ -9,11 +9,8 @@ import { me, reduceMotion } from 'flavours/glitch/initial_state';
|
|||||||
import ready from 'flavours/glitch/ready';
|
import ready from 'flavours/glitch/ready';
|
||||||
import { store } from 'flavours/glitch/store';
|
import { store } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import {
|
import { initializeEmoji } from './features/emoji';
|
||||||
isProduction,
|
import { isProduction, isDevelopment } from './utils/environment';
|
||||||
isDevelopment,
|
|
||||||
isModernEmojiEnabled,
|
|
||||||
} from './utils/environment';
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
@ -33,12 +30,7 @@ function main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isModernEmojiEnabled()) {
|
initializeEmoji();
|
||||||
const { initializeEmoji } = await import(
|
|
||||||
'@/flavours/glitch/features/emoji'
|
|
||||||
);
|
|
||||||
initializeEmoji();
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
root.render(<Mastodon {...props} />);
|
root.render(<Mastodon {...props} />);
|
||||||
|
|||||||
@ -8,11 +8,10 @@ import type {
|
|||||||
ApiAccountRoleJSON,
|
ApiAccountRoleJSON,
|
||||||
ApiAccountJSON,
|
ApiAccountJSON,
|
||||||
} from 'flavours/glitch/api_types/accounts';
|
} from 'flavours/glitch/api_types/accounts';
|
||||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
|
||||||
import { unescapeHTML } from 'flavours/glitch/utils/html';
|
import { unescapeHTML } from 'flavours/glitch/utils/html';
|
||||||
|
|
||||||
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
// AccountField
|
// AccountField
|
||||||
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
|
||||||
@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = {
|
|||||||
|
|
||||||
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
||||||
|
|
||||||
function createAccountField(
|
function createAccountField(jsonField: ApiAccountFieldJSON) {
|
||||||
jsonField: ApiAccountFieldJSON,
|
|
||||||
emojiMap: EmojiMap,
|
|
||||||
) {
|
|
||||||
return AccountFieldFactory({
|
return AccountFieldFactory({
|
||||||
...jsonField,
|
...jsonField,
|
||||||
name_emojified: emojify(
|
name_emojified: escapeTextContentForBrowser(jsonField.name),
|
||||||
escapeTextContentForBrowser(jsonField.name),
|
value_emojified: jsonField.value,
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
value_emojified: emojify(jsonField.value, emojiMap),
|
|
||||||
value_plain: unescapeHTML(jsonField.value),
|
value_plain: unescapeHTML(jsonField.value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -120,8 +113,6 @@ function createAccountField(
|
|||||||
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
const { moved, ...accountJSON } = serverJSON;
|
const { moved, ...accountJSON } = serverJSON;
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(accountJSON.emojis);
|
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
accountJSON.display_name.trim().length === 0
|
accountJSON.display_name.trim().length === 0
|
||||||
? accountJSON.username
|
? accountJSON.username
|
||||||
@ -134,7 +125,7 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
|||||||
...accountJSON,
|
...accountJSON,
|
||||||
moved: moved?.id,
|
moved: moved?.id,
|
||||||
fields: ImmutableList(
|
fields: ImmutableList(
|
||||||
serverJSON.fields.map((field) => createAccountField(field, emojiMap)),
|
serverJSON.fields.map((field) => createAccountField(field)),
|
||||||
),
|
),
|
||||||
emojis: ImmutableList(
|
emojis: ImmutableList(
|
||||||
serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
|
serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
|
||||||
@ -142,11 +133,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
|||||||
roles: ImmutableList(
|
roles: ImmutableList(
|
||||||
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
serverJSON.roles?.map((role) => AccountRoleFactory(role)),
|
||||||
),
|
),
|
||||||
display_name_html: emojify(
|
display_name_html: escapeTextContentForBrowser(displayName),
|
||||||
escapeTextContentForBrowser(displayName),
|
note_emojified: accountNote,
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
note_emojified: emojify(accountNote, emojiMap),
|
|
||||||
note_plain: unescapeHTML(accountNote),
|
note_plain: unescapeHTML(accountNote),
|
||||||
url:
|
url:
|
||||||
accountJSON.url?.startsWith('http://') ||
|
accountJSON.url?.startsWith('http://') ||
|
||||||
|
|||||||
@ -4,10 +4,9 @@ import type {
|
|||||||
ApiPollJSON,
|
ApiPollJSON,
|
||||||
ApiPollOptionJSON,
|
ApiPollOptionJSON,
|
||||||
} from 'flavours/glitch/api_types/polls';
|
} from 'flavours/glitch/api_types/polls';
|
||||||
import emojify from 'flavours/glitch/features/emoji/emoji';
|
|
||||||
|
|
||||||
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
|
import { CustomEmojiFactory } from './custom_emoji';
|
||||||
import type { CustomEmoji, EmojiMap } from './custom_emoji';
|
import type { CustomEmoji } from './custom_emoji';
|
||||||
|
|
||||||
interface PollOptionTranslation {
|
interface PollOptionTranslation {
|
||||||
title: string;
|
title: string;
|
||||||
@ -20,16 +19,12 @@ export interface PollOption extends ApiPollOptionJSON {
|
|||||||
translation: PollOptionTranslation | null;
|
translation: PollOptionTranslation | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPollOptionTranslationFromServerJSON(
|
export function createPollOptionTranslationFromServerJSON(translation: {
|
||||||
translation: { title: string },
|
title: string;
|
||||||
emojiMap: EmojiMap,
|
}) {
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
...translation,
|
...translation,
|
||||||
titleHtml: emojify(
|
titleHtml: escapeTextContentForBrowser(translation.title),
|
||||||
escapeTextContentForBrowser(translation.title),
|
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
} as PollOptionTranslation;
|
} as PollOptionTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +48,6 @@ export function createPollFromServerJSON(
|
|||||||
serverJSON: ApiPollJSON,
|
serverJSON: ApiPollJSON,
|
||||||
previousPoll?: Poll,
|
previousPoll?: Poll,
|
||||||
) {
|
) {
|
||||||
const emojiMap = makeEmojiMap(serverJSON.emojis);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...pollDefaultValues,
|
...pollDefaultValues,
|
||||||
...serverJSON,
|
...serverJSON,
|
||||||
@ -63,20 +56,15 @@ export function createPollFromServerJSON(
|
|||||||
const option = {
|
const option = {
|
||||||
...optionJSON,
|
...optionJSON,
|
||||||
voted: serverJSON.own_votes?.includes(index) || false,
|
voted: serverJSON.own_votes?.includes(index) || false,
|
||||||
titleHtml: emojify(
|
titleHtml: escapeTextContentForBrowser(optionJSON.title),
|
||||||
escapeTextContentForBrowser(optionJSON.title),
|
|
||||||
emojiMap,
|
|
||||||
),
|
|
||||||
} as PollOption;
|
} as PollOption;
|
||||||
|
|
||||||
const prevOption = previousPoll?.options[index];
|
const prevOption = previousPoll?.options[index];
|
||||||
if (prevOption?.translation && prevOption.title === option.title) {
|
if (prevOption?.translation && prevOption.title === option.title) {
|
||||||
const { translation } = prevOption;
|
const { translation } = prevOption;
|
||||||
|
|
||||||
option.translation = createPollOptionTranslationFromServerJSON(
|
option.translation =
|
||||||
translation,
|
createPollOptionTranslationFromServerJSON(translation);
|
||||||
emojiMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
|
|||||||
@ -2,9 +2,6 @@
|
|||||||
// If there are no polyfills, then this is just Promise.resolve() which means
|
// If there are no polyfills, then this is just Promise.resolve() which means
|
||||||
// it will execute in the same tick of the event loop (i.e. near-instant).
|
// it will execute in the same tick of the event loop (i.e. near-instant).
|
||||||
|
|
||||||
// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension
|
|
||||||
import 'vite/modulepreload-polyfill';
|
|
||||||
|
|
||||||
import { loadIntlPolyfills } from './intl';
|
import { loadIntlPolyfills } from './intl';
|
||||||
|
|
||||||
function importExtraPolyfills() {
|
function importExtraPolyfills() {
|
||||||
@ -17,6 +14,7 @@ export function loadPolyfills() {
|
|||||||
const needsExtraPolyfills = !window.requestIdleCallback;
|
const needsExtraPolyfills = !window.requestIdleCallback;
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
loadVitePreloadPolyfill(),
|
||||||
loadIntlPolyfills(),
|
loadIntlPolyfills(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
|
||||||
needsExtraPolyfills ? importExtraPolyfills() : Promise.resolve(),
|
needsExtraPolyfills ? importExtraPolyfills() : Promise.resolve(),
|
||||||
@ -31,5 +29,13 @@ async function loadEmojiPolyfills() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loads Vite's module preload polyfill for older browsers, but not in a Worker context.
|
||||||
|
function loadVitePreloadPolyfill() {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
// @ts-expect-error -- This is a virtual module provided by Vite.
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
return import('vite/modulepreload-polyfill');
|
||||||
|
}
|
||||||
|
|
||||||
// Null unless polyfill is needed.
|
// Null unless polyfill is needed.
|
||||||
export let emojiRegexPolyfill: RegExp | null = null;
|
export let emojiRegexPolyfill: RegExp | null = null;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { Reducer } from '@reduxjs/toolkit';
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { importPolls } from 'flavours/glitch/actions/importer/polls';
|
import { importPolls } from 'flavours/glitch/actions/importer/polls';
|
||||||
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
|
|
||||||
import { createPollOptionTranslationFromServerJSON } from 'flavours/glitch/models/poll';
|
import { createPollOptionTranslationFromServerJSON } from 'flavours/glitch/models/poll';
|
||||||
import type { Poll } from 'flavours/glitch/models/poll';
|
import type { Poll } from 'flavours/glitch/models/poll';
|
||||||
|
|
||||||
@ -20,16 +19,11 @@ const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => {
|
|||||||
|
|
||||||
if (!poll) return;
|
if (!poll) return;
|
||||||
|
|
||||||
const emojiMap = makeEmojiMap(poll.emojis);
|
|
||||||
|
|
||||||
pollTranslation.options.forEach((item, index) => {
|
pollTranslation.options.forEach((item, index) => {
|
||||||
const option = poll.options[index];
|
const option = poll.options[index];
|
||||||
if (!option) return;
|
if (!option) return;
|
||||||
|
|
||||||
option.translation = createPollOptionTranslationFromServerJSON(
|
option.translation = createPollOptionTranslationFromServerJSON(item);
|
||||||
item,
|
|
||||||
emojiMap,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -138,10 +138,15 @@ const channelNameWithInlineParams = (channelName, params) => {
|
|||||||
return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`;
|
return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('flavours/glitch/store').AppDispatch} Dispatch
|
||||||
|
* @typedef {import('flavours/glitch/store').GetState} GetState
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
|
* @param {function(Dispatch, GetState): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
|
||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@ -209,7 +214,6 @@ const KNOWN_EVENT_TYPES = [
|
|||||||
'notification',
|
'notification',
|
||||||
'conversation',
|
'conversation',
|
||||||
'filters_changed',
|
'filters_changed',
|
||||||
'encrypted_message',
|
|
||||||
'announcement',
|
'announcement',
|
||||||
'announcement.delete',
|
'announcement.delete',
|
||||||
'announcement.reaction',
|
'announcement.reaction',
|
||||||
@ -230,7 +234,7 @@ const handleEventSourceMessage = (e, received) => {
|
|||||||
* @param {string} streamingAPIBaseURL
|
* @param {string} streamingAPIBaseURL
|
||||||
* @param {string} accessToken
|
* @param {string} accessToken
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks
|
* @param {{ connected: function(): void, received: function(StreamEvent): void, disconnected: function(): void, reconnected: function(): void }} callbacks
|
||||||
* @returns {WebSocketClient | EventSource}
|
* @returns {WebSocketClient | EventSource}
|
||||||
*/
|
*/
|
||||||
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
|
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
|
||||||
@ -243,12 +247,9 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
ws.onopen = connected;
|
ws.onopen = connected;
|
||||||
ws.onmessage = e => received(JSON.parse(e.data));
|
ws.onmessage = e => received(JSON.parse(e.data));
|
||||||
// @ts-expect-error
|
|
||||||
ws.onclose = disconnected;
|
ws.onclose = disconnected;
|
||||||
// @ts-expect-error
|
|
||||||
ws.onreconnect = reconnected;
|
ws.onreconnect = reconnected;
|
||||||
|
|
||||||
return ws;
|
return ws;
|
||||||
|
|||||||
@ -4096,6 +4096,7 @@ a.account__display-name {
|
|||||||
background: lighten($ui-highlight-color, 5%);
|
background: lighten($ui-highlight-color, 5%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.follow_requests-unlocked_explanation,
|
||||||
.switch-to-advanced {
|
.switch-to-advanced {
|
||||||
color: $light-text-color;
|
color: $light-text-color;
|
||||||
background-color: $ui-base-color;
|
background-color: $ui-base-color;
|
||||||
@ -4106,7 +4107,7 @@ a.account__display-name {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
||||||
.switch-to-advanced__toggle {
|
a {
|
||||||
color: $ui-button-tertiary-color;
|
color: $ui-button-tertiary-color;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -5355,8 +5356,7 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-column-indicator,
|
.empty-column-indicator {
|
||||||
.follow_requests-unlocked_explanation {
|
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@ -5395,10 +5395,8 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.follow_requests-unlocked_explanation {
|
.follow_requests-unlocked_explanation {
|
||||||
background: var(--surface-background-color);
|
margin: 16px;
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
margin-bottom: 0;
|
||||||
contain: initial;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-column {
|
.error-column {
|
||||||
|
|||||||
@ -12,16 +12,8 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures';
|
export type Features = 'fasp' | 'http_message_signatures';
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isFeatureEnabled(feature: Features) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isModernEmojiEnabled() {
|
|
||||||
try {
|
|
||||||
return isFeatureEnabled('modern_emojis');
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export function recoverHashtags (recognizedTags, text) {
|
export function recoverHashtags (recognizedTags, text) {
|
||||||
return recognizedTags.map(tag => {
|
return recognizedTags.map(tag => {
|
||||||
const re = new RegExp(`(?:^|[^/)\\w])#(${tag.name})`, 'i');
|
const re = new RegExp(`(?:^|[^/)\\w])[##](${tag.name})`, 'i');
|
||||||
const matched_hashtag = text.match(re);
|
const matched_hashtag = text.match(re);
|
||||||
return matched_hashtag ? matched_hashtag[1] : null;
|
return matched_hashtag ? matched_hashtag[1] : null;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -624,6 +624,7 @@ export function fetchComposeSuggestions(token) {
|
|||||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
case '#':
|
case '#':
|
||||||
|
case '#':
|
||||||
fetchComposeSuggestionsTags(dispatch, getState, token);
|
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -665,11 +666,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
} else if (suggestion.type === 'hashtag') {
|
} else if (suggestion.type === 'hashtag') {
|
||||||
completion = `#${suggestion.name}`;
|
completion = suggestion.name.slice(token.length - 1);
|
||||||
startPosition = position - 1;
|
startPosition = position + token.length;
|
||||||
} else if (suggestion.type === 'account') {
|
} else if (suggestion.type === 'account') {
|
||||||
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
|
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
|
||||||
startPosition = position;
|
startPosition = position - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||||
@ -729,7 +730,7 @@ function insertIntoTagHistory(recognizedTags, text) {
|
|||||||
// complicated because of new normalization rules, it's no longer just
|
// complicated because of new normalization rules, it's no longer just
|
||||||
// a case sensitivity issue
|
// a case sensitivity issue
|
||||||
const names = recognizedTags.map(tag => {
|
const names = recognizedTags.map(tag => {
|
||||||
const matches = text.match(new RegExp(`#${tag.name}`, 'i'));
|
const matches = text.match(new RegExp(`[##]${tag.name}`, 'i'));
|
||||||
|
|
||||||
if (matches && matches.length > 0) {
|
if (matches && matches.length > 0) {
|
||||||
return matches[0].slice(1);
|
return matches[0].slice(1);
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
|
||||||
import { expandSpoilers } from '../../initial_state';
|
import { expandSpoilers } from '../../initial_state';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
|
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = normalStatus.content;
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
|
||||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||||
|
|
||||||
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
@ -128,14 +124,12 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStatusTranslation(translation, status) {
|
export function normalizeStatusTranslation(translation, status) {
|
||||||
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
|
|
||||||
|
|
||||||
const normalTranslation = {
|
const normalTranslation = {
|
||||||
detected_source_language: translation.detected_source_language,
|
detected_source_language: translation.detected_source_language,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
provider: translation.provider,
|
provider: translation.provider,
|
||||||
contentHtml: emojify(translation.content, emojiMap),
|
contentHtml: translation.content,
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
|
||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) {
|
|||||||
|
|
||||||
export function normalizeAnnouncement(announcement) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = normalAnnouncement.content;
|
||||||
|
|
||||||
return normalAnnouncement;
|
return normalAnnouncement;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,13 +32,20 @@ import {
|
|||||||
const randomUpTo = max =>
|
const randomUpTo = max =>
|
||||||
Math.floor(Math.random() * Math.floor(max));
|
Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('mastodon/store').AppDispatch} Dispatch
|
||||||
|
* @typedef {import('mastodon/store').GetState} GetState
|
||||||
|
* @typedef {import('redux').UnknownAction} UnknownAction
|
||||||
|
* @typedef {function(Dispatch, GetState): Promise<void>} FallbackFunction
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} timelineId
|
* @param {string} timelineId
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function, Function): Promise<void>} [options.fallback]
|
* @param {FallbackFunction} [options.fallback]
|
||||||
* @param {function(): void} [options.fillGaps]
|
* @param {function(): UnknownAction} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
@ -46,13 +53,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
|
||||||
return connectStream(channelName, params, (dispatch, getState) => {
|
return connectStream(channelName, params, (dispatch, getState) => {
|
||||||
|
// @ts-ignore
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
let pollingId;
|
let pollingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function(Function, Function): Promise<void>} fallback
|
* @param {FallbackFunction} fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const useFallback = async fallback => {
|
const useFallback = async fallback => {
|
||||||
@ -132,7 +140,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} dispatch
|
* @param {Dispatch} dispatch
|
||||||
*/
|
*/
|
||||||
async function refreshHomeTimelineAndNotification(dispatch) {
|
async function refreshHomeTimelineAndNotification(dispatch) {
|
||||||
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
||||||
@ -151,7 +159,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
connectTimelineStream('home', 'user', {}, {
|
||||||
|
fallback: refreshHomeTimelineAndNotification,
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: fillHomeTimelineGaps
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -159,7 +171,10 @@ export const connectUserStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@ -168,7 +183,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) =>
|
||||||
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) });
|
connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote })
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} columnId
|
* @param {string} columnId
|
||||||
@ -191,4 +209,7 @@ export const connectDirectStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectListStream = listId =>
|
export const connectListStream = listId =>
|
||||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillListTimelineGaps(listId)
|
||||||
|
});
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
|
||||||
|
|
||||||
import { useAppSelector } from '../store';
|
import { useAppSelector } from '../store';
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { useElementHandledLink } from './status/handled_link';
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
@ -21,22 +16,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
accountId,
|
accountId,
|
||||||
showDropdown = false,
|
showDropdown = false,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useLinks(showDropdown);
|
|
||||||
const handleNodeChange = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (
|
|
||||||
!showDropdown ||
|
|
||||||
!node ||
|
|
||||||
node.childNodes.length === 0 ||
|
|
||||||
isModernEmojiEnabled()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addDropdownToHashtags(node, accountId);
|
|
||||||
},
|
|
||||||
[showDropdown, accountId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: showDropdown ? accountId : undefined,
|
hashtagAccountId: showDropdown ? accountId : undefined,
|
||||||
});
|
});
|
||||||
@ -62,30 +41,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
htmlString={note}
|
htmlString={note}
|
||||||
extraEmojis={extraEmojis}
|
extraEmojis={extraEmojis}
|
||||||
className={classNames(className, 'translate')}
|
className={classNames(className, 'translate')}
|
||||||
onClickCapture={handleClick}
|
|
||||||
ref={handleNodeChange}
|
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const childNode of node.childNodes) {
|
|
||||||
if (!(childNode instanceof HTMLElement)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
childNode instanceof HTMLAnchorElement &&
|
|
||||||
(childNode.classList.contains('hashtag') ||
|
|
||||||
childNode.innerText.startsWith('#')) &&
|
|
||||||
!childNode.dataset.menuHashtag
|
|
||||||
) {
|
|
||||||
childNode.dataset.menuHashtag = accountId;
|
|
||||||
} else if (childNode.childNodes.length > 0) {
|
|
||||||
addDropdownToHashtags(childNode, accountId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
searchTokens: ['@', ':', '#'],
|
searchTokens: ['@', '@', ':', '#', '#'],
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || ['@', '@', ':', '#', '#'].indexOf(word[0]) === -1) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,6 @@ export const Linked: Story = {
|
|||||||
acct: username,
|
acct: username,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
return <LinkedDisplayName {...args} displayProps={{ account }} />;
|
return <LinkedDisplayName displayProps={{ account, ...args }} />;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameWithoutDomain: FC<
|
export const DisplayNameWithoutDomain: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, className, children, localDomain: _, ...props }) => {
|
||||||
> = ({ account, className, children, ...props }) => {
|
|
||||||
return (
|
return (
|
||||||
<AnimateEmojiProvider
|
<AnimateEmojiProvider
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -5,9 +5,8 @@ import { EmojiHTML } from '../emoji/html';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameSimple: FC<
|
export const DisplayNameSimple: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, localDomain: _, ...props }) => {
|
||||||
> = ({ account, ...props }) => {
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
|
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
|
||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
const parentContext = useContext(AnimateEmojiContext);
|
const parentContext = useContext(AnimateEmojiContext);
|
||||||
if (parentContext !== null) {
|
if (parentContext !== null) {
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper {...props} className={className} ref={ref}>
|
||||||
{...props}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef<
|
|||||||
return (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(className, 'animate-parent')}
|
className={className}
|
||||||
onMouseEnter={handleEnter}
|
onMouseEnter={handleEnter}
|
||||||
onMouseLeave={handleLeave}
|
onMouseLeave={handleLeave}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import type {
|
import type {
|
||||||
OnAttributeHandler,
|
OnAttributeHandler,
|
||||||
OnElementHandler,
|
OnElementHandler,
|
||||||
@ -22,7 +19,7 @@ export interface EmojiHTMLProps {
|
|||||||
onAttribute?: OnAttributeHandler;
|
onAttribute?: OnAttributeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
extraEmojis,
|
extraEmojis,
|
||||||
@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
ModernEmojiHTML.displayName = 'ModernEmojiHTML';
|
EmojiHTML.displayName = 'EmojiHTML';
|
||||||
|
|
||||||
export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>(
|
|
||||||
(props, ref) => {
|
|
||||||
const {
|
|
||||||
as: asElement,
|
|
||||||
htmlString,
|
|
||||||
extraEmojis,
|
|
||||||
className,
|
|
||||||
onElement,
|
|
||||||
onAttribute,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
const Wrapper = asElement ?? 'div';
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
{...rest}
|
|
||||||
ref={ref}
|
|
||||||
dangerouslySetInnerHTML={{ __html: htmlString }}
|
|
||||||
className={classNames(className, 'animate-parent')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
LegacyEmojiHTML.displayName = 'LegacyEmojiHTML';
|
|
||||||
|
|
||||||
export const EmojiHTML = isModernEmojiEnabled()
|
|
||||||
? ModernEmojiHTML
|
|
||||||
: LegacyEmojiHTML;
|
|
||||||
|
|||||||
@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state';
|
|||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { useLinks } from '../hooks/useLinks';
|
|
||||||
|
|
||||||
export const HoverCardAccount = forwardRef<
|
export const HoverCardAccount = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{ accountId?: string }
|
{ accountId?: string }
|
||||||
@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef<
|
|||||||
!isMutual &&
|
!isMutual &&
|
||||||
!isFollower;
|
!isFollower;
|
||||||
|
|
||||||
const handleClick = useLinks();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -110,7 +106,7 @@ export const HoverCardAccount = forwardRef<
|
|||||||
className='hover-card__bio'
|
className='hover-card__bio'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='account-fields' onClickCapture={handleClick}>
|
<div className='account-fields'>
|
||||||
<AccountFields
|
<AccountFields
|
||||||
fields={account.fields.take(2)}
|
fields={account.fields.take(2)}
|
||||||
emojis={account.emojis}
|
emojis={account.emojis}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { OnElementHandler } from '@/mastodon/utils/html';
|
|||||||
import { polymorphicForwardRef } from '@/types/polymorphic';
|
import { polymorphicForwardRef } from '@/types/polymorphic';
|
||||||
|
|
||||||
import type { EmojiHTMLProps } from '../emoji/html';
|
import type { EmojiHTMLProps } from '../emoji/html';
|
||||||
import { ModernEmojiHTML } from '../emoji/html';
|
import { EmojiHTML } from '../emoji/html';
|
||||||
import { useElementHandledLink } from '../status/handled_link';
|
import { useElementHandledLink } from '../status/handled_link';
|
||||||
|
|
||||||
export const HTMLBlock = polymorphicForwardRef<
|
export const HTMLBlock = polymorphicForwardRef<
|
||||||
@ -25,6 +25,6 @@ export const HTMLBlock = polymorphicForwardRef<
|
|||||||
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
(...args) => onParentElement?.(...args) ?? onLinkElement(...args),
|
||||||
[onLinkElement, onParentElement],
|
[onLinkElement, onParentElement],
|
||||||
);
|
);
|
||||||
return <ModernEmojiHTML {...props} onElement={onElement} />;
|
return <EmojiHTML {...props} onElement={onElement} />;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
|
||||||
import type * as Model from 'mastodon/models/poll';
|
import type * as Model from 'mastodon/models/poll';
|
||||||
import type { Status } from 'mastodon/models/status';
|
import type { Status } from 'mastodon/models/status';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
@ -235,12 +233,11 @@ const PollOption: React.FC<PollOptionProps> = (props) => {
|
|||||||
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
let titleHtml = option.translation?.titleHtml ?? option.titleHtml;
|
||||||
|
|
||||||
if (!titleHtml) {
|
if (!titleHtml) {
|
||||||
const emojiMap = makeEmojiMap(poll.emojis);
|
titleHtml = escapeTextContentForBrowser(title);
|
||||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return titleHtml;
|
return titleHtml;
|
||||||
}, [option, poll, title]);
|
}, [option, title]);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleOptionChange = useCallback(() => {
|
const handleOptionChange = useCallback(() => {
|
||||||
|
|||||||
@ -26,7 +26,12 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
|
|||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
// Handle hashtags
|
// Handle hashtags
|
||||||
if (text.startsWith('#') || prevText?.endsWith('#')) {
|
if (
|
||||||
|
text.startsWith('#') ||
|
||||||
|
prevText?.endsWith('#') ||
|
||||||
|
text.startsWith('#') ||
|
||||||
|
prevText?.endsWith('#')
|
||||||
|
) {
|
||||||
const hashtag = text.slice(1).trim();
|
const hashtag = text.slice(1).trim();
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@ -15,8 +15,6 @@ import { Poll } from 'mastodon/components/poll';
|
|||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { HandledLink } from './status/handled_link';
|
import { HandledLink } from './status/handled_link';
|
||||||
|
|
||||||
@ -72,6 +70,17 @@ const mapStateToProps = state => ({
|
|||||||
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
languages: state.getIn(['server', 'translationLanguages', 'items']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const compareUrls = (href1, href2) => {
|
||||||
|
try {
|
||||||
|
const url1 = new URL(href1);
|
||||||
|
const url2 = new URL(href2);
|
||||||
|
|
||||||
|
return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class StatusContent extends PureComponent {
|
class StatusContent extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
identity: identityContextPropShape,
|
identity: identityContextPropShape,
|
||||||
@ -108,41 +117,6 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
onCollapsedToggle(collapsed);
|
onCollapsedToggle(collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if modern emoji is enabled, as it handles links using the HandledLink component.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
let link, mention;
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', `@${mention.get('acct')}`);
|
|
||||||
link.setAttribute('href', `/@${mention.get('acct')}`);
|
|
||||||
link.setAttribute('data-hover-card-account', mention.get('id'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
|
||||||
link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id']));
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@ -153,22 +127,6 @@ class StatusContent extends PureComponent {
|
|||||||
this._updateStatusLinks();
|
this._updateStatusLinks();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseDown = (e) => {
|
handleMouseDown = (e) => {
|
||||||
this.startXY = [e.clientX, e.clientY];
|
this.startXY = [e.clientX, e.clientY];
|
||||||
};
|
};
|
||||||
@ -206,7 +164,7 @@ class StatusContent extends PureComponent {
|
|||||||
|
|
||||||
handleElement = (element, { key, ...props }, children) => {
|
handleElement = (element, { key, ...props }, children) => {
|
||||||
if (element instanceof HTMLAnchorElement) {
|
if (element instanceof HTMLAnchorElement) {
|
||||||
const mention = this.props.status.get('mentions').find(item => element.href === item.get('url'));
|
const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url')));
|
||||||
return (
|
return (
|
||||||
<HandledLink
|
<HandledLink
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -1,30 +1,10 @@
|
|||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
import type { OnAttributeHandler } from '../utils/html';
|
import type { OnAttributeHandler } from '../utils/html';
|
||||||
|
|
||||||
import { Icon } from './icon';
|
import { Icon } from './icon';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
|
||||||
|
|
||||||
const stripRelMe = (html: string) => {
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
|
||||||
|
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
|
|
||||||
link.rel = link.rel
|
|
||||||
.split(' ')
|
|
||||||
.filter((x: string) => x !== 'me')
|
|
||||||
.join(' ');
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = document.querySelector('body');
|
|
||||||
return body?.innerHTML ?? '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
const onAttribute: OnAttributeHandler = (name, value, tagName) => {
|
||||||
if (name === 'rel' && tagName === 'a') {
|
if (name === 'rel' && tagName === 'a') {
|
||||||
if (value === 'me') {
|
if (value === 'me') {
|
||||||
@ -47,10 +27,6 @@ interface Props {
|
|||||||
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
|
||||||
<span className='verified-badge'>
|
<span className='verified-badge'>
|
||||||
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
<Icon id='check' icon={CheckIcon} className='verified-badge__mark' />
|
||||||
<EmojiHTML
|
<EmojiHTML as='span' htmlString={link} onAttribute={onAttribute} />
|
||||||
as='span'
|
|
||||||
htmlString={stripRelMe(link)}
|
|
||||||
onAttribute={onAttribute}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number';
|
|||||||
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
import { AccountNote } from 'mastodon/features/account/components/account_note';
|
||||||
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
|
||||||
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{
|
|||||||
state.relationships.get(accountId),
|
state.relationships.get(accountId),
|
||||||
);
|
);
|
||||||
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
|
||||||
const handleLinkClick = useLinks();
|
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
const handleBlock = useCallback(() => {
|
||||||
if (!account) {
|
if (!account) {
|
||||||
@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{
|
|||||||
|
|
||||||
{!(suspended || hidden) && (
|
{!(suspended || hidden) && (
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div
|
<div className='account__header__bio'>
|
||||||
className='account__header__bio'
|
|
||||||
onClickCapture={handleLinkClick}
|
|
||||||
>
|
|
||||||
{account.id !== me && signedIn && (
|
{account.id !== me && signedIn && (
|
||||||
<AccountNote accountId={accountId} />
|
<AccountNote accountId={accountId} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { autoPlayGif } from '../../initial_state';
|
import { autoPlayGif } from '../../initial_state';
|
||||||
@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => {
|
|||||||
* Legacy emoji processing function.
|
* Legacy emoji processing function.
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {object} customEmojis
|
* @param {object} customEmojis
|
||||||
* @param {boolean} force If true, always emojify even if modern emoji is enabled
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
const emojify = (str, customEmojis = {}, force = false) => {
|
const emojify = (str, customEmojis = {}) => {
|
||||||
if (isModernEmojiEnabled() && !force) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.innerHTML = str;
|
wrapper.innerHTML = str;
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
|||||||
|
|
||||||
import data from './emoji_data.json';
|
import data from './emoji_data.json';
|
||||||
import emojiMap from './emoji_map.json';
|
import emojiMap from './emoji_map.json';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
|
||||||
|
|
||||||
emojiMartUncompress(data);
|
emojiMartUncompress(data);
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type {
|
|||||||
ShortCodesToEmojiData,
|
ShortCodesToEmojiData,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_utils';
|
||||||
|
|
||||||
type Emojis = Record<
|
type Emojis = Record<
|
||||||
NonNullable<keyof ShortCodesToEmojiData>,
|
NonNullable<keyof ShortCodesToEmojiData>,
|
||||||
@ -23,7 +23,7 @@ type Emojis = Record<
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
skins,
|
_skins,
|
||||||
categories,
|
categories,
|
||||||
short_names,
|
short_names,
|
||||||
_emojisWithoutShortCodes,
|
_emojisWithoutShortCodes,
|
||||||
@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export { emojis, skins, categories, short_names };
|
export { emojis, categories, short_names };
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// This code is largely borrowed from:
|
// This code is largely borrowed from:
|
||||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
||||||
|
|
||||||
import * as data from './emoji_mart_data_light';
|
import { emojis, categories } from './emoji_mart_data_light';
|
||||||
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
||||||
|
|
||||||
let originalPool = {};
|
let originalPool = {};
|
||||||
@ -10,8 +10,8 @@ let emojisList = {};
|
|||||||
let emoticonsList = {};
|
let emoticonsList = {};
|
||||||
let customEmojisList = [];
|
let customEmojisList = [];
|
||||||
|
|
||||||
for (let emoji in data.emojis) {
|
for (let emoji in emojis) {
|
||||||
let emojiData = data.emojis[emoji];
|
let emojiData = emojis[emoji];
|
||||||
let { short_names, emoticons } = emojiData;
|
let { short_names, emoticons } = emojiData;
|
||||||
let id = short_names[0];
|
let id = short_names[0];
|
||||||
|
|
||||||
@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
if (include.length || exclude.length) {
|
if (include.length || exclude.length) {
|
||||||
pool = {};
|
pool = {};
|
||||||
|
|
||||||
data.categories.forEach(category => {
|
categories.forEach(category => {
|
||||||
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
||||||
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
||||||
if (!isIncluded || isExcluded) {
|
if (!isIncluded || isExcluded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
|
category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (custom.length) {
|
if (custom.length) {
|
||||||
@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
|
|||||||
|
|
||||||
if (results) {
|
if (results) {
|
||||||
if (emojisToShowFilter) {
|
if (emojisToShowFilter) {
|
||||||
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
|
results = results.filter((result) => emojisToShowFilter(emojis[result.id]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (results && results.length > maxResults) {
|
if (results && results.length > maxResults) {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart';
|
|||||||
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
|
||||||
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { EMOJI_MODE_NATIVE } from './constants';
|
import { EMOJI_MODE_NATIVE } from './constants';
|
||||||
@ -27,7 +26,7 @@ const Emoji = ({
|
|||||||
sheetSize={sheetSize}
|
sheetSize={sheetSize}
|
||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@ -51,7 +50,7 @@ const Picker = ({
|
|||||||
sheetColumns={sheetColumns}
|
sheetColumns={sheetColumns}
|
||||||
sheetRows={sheetRows}
|
sheetRows={sheetRows}
|
||||||
backgroundImageFn={backgroundImageFn}
|
backgroundImageFn={backgroundImageFn}
|
||||||
native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()}
|
native={mode === EMOJI_MODE_NATIVE}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type {
|
|||||||
ShortCodesToEmojiDataKey,
|
ShortCodesToEmojiDataKey,
|
||||||
} from 'virtual:mastodon-emoji-compressed';
|
} from 'virtual:mastodon-emoji-compressed';
|
||||||
|
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_utils';
|
||||||
|
|
||||||
type UnicodeMapping = Record<
|
type UnicodeMapping = Record<
|
||||||
FilenameData[number][0],
|
FilenameData[number][0],
|
||||||
|
|||||||
@ -209,50 +209,9 @@ function intersect(a, b) {
|
|||||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepMerge(a, b) {
|
|
||||||
let o = {};
|
|
||||||
|
|
||||||
for (let key in a) {
|
|
||||||
let originalValue = a[key],
|
|
||||||
value = originalValue;
|
|
||||||
|
|
||||||
if (Object.hasOwn(b, key)) {
|
|
||||||
value = b[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = deepMerge(originalValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
o[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/sonicdoe/measure-scrollbar
|
|
||||||
function measureScrollbar() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
|
|
||||||
div.style.width = '100px';
|
|
||||||
div.style.height = '100px';
|
|
||||||
div.style.overflow = 'scroll';
|
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.top = '-9999px';
|
|
||||||
|
|
||||||
document.body.appendChild(div);
|
|
||||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
|
||||||
document.body.removeChild(div);
|
|
||||||
|
|
||||||
return scrollbarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getData,
|
getData,
|
||||||
getSanitizedData,
|
getSanitizedData,
|
||||||
uniq,
|
uniq,
|
||||||
intersect,
|
intersect,
|
||||||
deepMerge,
|
|
||||||
unifiedToNative,
|
|
||||||
measureScrollbar,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
|
||||||
|
|
||||||
const PARENT_MAX_DEPTH = 10;
|
|
||||||
|
|
||||||
export function handleAnimateGif(event: MouseEvent) {
|
|
||||||
// We already check this in ui/index.jsx, but just to be sure.
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { target, type } = event;
|
|
||||||
const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate.
|
|
||||||
|
|
||||||
if (target instanceof HTMLImageElement) {
|
|
||||||
setAnimateGif(target, animate);
|
|
||||||
} else if (!(target instanceof HTMLElement) || target === document.body) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent: HTMLElement | null = null;
|
|
||||||
let iter = 0;
|
|
||||||
|
|
||||||
if (target.classList.contains('animate-parent')) {
|
|
||||||
parent = target;
|
|
||||||
} else {
|
|
||||||
// Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'.
|
|
||||||
let current: HTMLElement | null = target;
|
|
||||||
while (current) {
|
|
||||||
if (iter >= PARENT_MAX_DEPTH) {
|
|
||||||
return; // We can just exit right now.
|
|
||||||
}
|
|
||||||
current = current.parentElement;
|
|
||||||
if (current?.classList.contains('animate-parent')) {
|
|
||||||
parent = current;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
iter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Affect all animated children within the parent.
|
|
||||||
if (parent) {
|
|
||||||
const animatedChildren =
|
|
||||||
parent.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
|
||||||
for (const child of animatedChildren) {
|
|
||||||
setAnimateGif(child, animate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAnimateGif(image: HTMLImageElement, animate: boolean) {
|
|
||||||
const { classList, dataset } = image;
|
|
||||||
if (
|
|
||||||
!classList.contains('custom-emoji') ||
|
|
||||||
!dataset.static ||
|
|
||||||
!dataset.original
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
image.src = animate ? dataset.original : dataset.static;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { initialState } from '@/mastodon/initial_state';
|
import { initialState } from '@/mastodon/initial_state';
|
||||||
import { loadWorker } from '@/mastodon/utils/workers';
|
|
||||||
|
|
||||||
import { toSupportedLocale } from './locale';
|
import { toSupportedLocale } from './locale';
|
||||||
import { emojiLogger } from './utils';
|
import { emojiLogger } from './utils';
|
||||||
|
// eslint-disable-next-line import/default -- Importing via worker loader.
|
||||||
|
import EmojiWorker from './worker?worker&inline';
|
||||||
|
|
||||||
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
|
||||||
|
|
||||||
@ -16,9 +17,7 @@ export function initializeEmoji() {
|
|||||||
log('initializing emojis');
|
log('initializing emojis');
|
||||||
if (!worker && 'Worker' in window) {
|
if (!worker && 'Worker' in window) {
|
||||||
try {
|
try {
|
||||||
worker = loadWorker(new URL('./worker', import.meta.url), {
|
worker = new EmojiWorker();
|
||||||
type: 'module',
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Error creating web worker:', err);
|
console.warn('Error creating web worker:', err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
// taken from:
|
|
||||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
|
||||||
export const unicodeToFilename = (str) => {
|
|
||||||
let result = '';
|
|
||||||
let charCode = 0;
|
|
||||||
let p = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (i < str.length) {
|
|
||||||
charCode = str.charCodeAt(i++);
|
|
||||||
if (p) {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
|
|
||||||
p = 0;
|
|
||||||
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
|
|
||||||
p = charCode;
|
|
||||||
} else {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += charCode.toString(16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
function padLeft(str, num) {
|
|
||||||
while (str.length < num) {
|
|
||||||
str = '0' + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const unicodeToUnifiedName = (str) => {
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
|
||||||
if (i > 0) {
|
|
||||||
output += '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
43
app/javascript/mastodon/features/emoji/unicode_utils.ts
Normal file
43
app/javascript/mastodon/features/emoji/unicode_utils.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// taken from:
|
||||||
|
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
||||||
|
export function unicodeToFilename(str: string) {
|
||||||
|
let result = '';
|
||||||
|
let charCode = 0;
|
||||||
|
let p = 0;
|
||||||
|
let i = 0;
|
||||||
|
while (i < str.length) {
|
||||||
|
charCode = str.charCodeAt(i++);
|
||||||
|
if (p) {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString(
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
p = 0;
|
||||||
|
} else if (0xd800 <= charCode && charCode <= 0xdbff) {
|
||||||
|
p = charCode;
|
||||||
|
} else {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result += '-';
|
||||||
|
}
|
||||||
|
result += charCode.toString(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unicodeToUnifiedName(str: string) {
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < str.length; i += 2) {
|
||||||
|
if (i > 0) {
|
||||||
|
output += '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
output +=
|
||||||
|
str.codePointAt(i)?.toString(16).toUpperCase().padStart(4, '0') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
@ -24,6 +24,14 @@ import StatusListContainer from '../ui/containers/status_list_container';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
title: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
|
title_local: {
|
||||||
|
id: 'column.firehose_local',
|
||||||
|
defaultMessage: 'Live feed for this server',
|
||||||
|
},
|
||||||
|
title_singular: {
|
||||||
|
id: 'column.firehose_singular',
|
||||||
|
defaultMessage: 'Live feed',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ColumnSettings = () => {
|
const ColumnSettings = () => {
|
||||||
@ -161,13 +169,23 @@ const Firehose = ({ feedType, multiColumn }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
if (canViewFeed(signedIn, permissions, localLiveFeedAccess) && canViewFeed(signedIn, permissions, remoteLiveFeedAccess)) {
|
||||||
|
title = messages.title;
|
||||||
|
} else if (canViewFeed(signedIn, permissions, localLiveFeedAccess)) {
|
||||||
|
title = messages.title_local;
|
||||||
|
} else {
|
||||||
|
title = messages.title_singular;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
icon='globe'
|
icon='globe'
|
||||||
iconComponent={PublicIcon}
|
iconComponent={PublicIcon}
|
||||||
active={hasUnread}
|
active={hasUnread}
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(title)}
|
||||||
onPin={handlePin}
|
onPin={handlePin}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
|
|||||||
@ -1,458 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent, useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { animated, useTransition } from '@react-spring/web';
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
|
||||||
|
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_picker_dropdown_container';
|
|
||||||
import { unicodeMapping } from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
|
||||||
import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'mastodon/initial_state';
|
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
|
||||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class ContentWithRouter extends ImmutablePureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._updateLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateLinks () {
|
|
||||||
const node = this.node;
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll('a');
|
|
||||||
|
|
||||||
for (var i = 0; i < links.length; ++i) {
|
|
||||||
let link = links[i];
|
|
||||||
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url'));
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
|
||||||
link.setAttribute('title', mention.get('acct'));
|
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
|
||||||
} else {
|
|
||||||
let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
|
|
||||||
if (status) {
|
|
||||||
link.addEventListener('click', this.onStatusClick.bind(this, status), false);
|
|
||||||
}
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
link.setAttribute('rel', 'noopener');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMentionClick = (mention, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${mention.get('acct')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onHashtagClick = (hashtag, e) => {
|
|
||||||
hashtag = hashtag.replace(/^#/, '');
|
|
||||||
|
|
||||||
if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/tags/${hashtag}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onStatusClick = (status, e) => {
|
|
||||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='announcements__item__content translate animate-parent'
|
|
||||||
ref={this.setRef}
|
|
||||||
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const Content = withRouter(ContentWithRouter);
|
|
||||||
|
|
||||||
class Emoji extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
emoji: PropTypes.string.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
hovered: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { emoji, emojiMap, hovered } = this.props;
|
|
||||||
|
|
||||||
if (unicodeMapping[emoji]) {
|
|
||||||
const { filename, shortCode } = unicodeMapping[this.props.emoji];
|
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione'
|
|
||||||
alt={emoji}
|
|
||||||
title={title}
|
|
||||||
src={`${assetHost}/emoji/${filename}.svg`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (emojiMap.get(emoji)) {
|
|
||||||
const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']);
|
|
||||||
const shortCode = `:${emoji}:`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
draggable='false'
|
|
||||||
className='emojione custom-emoji'
|
|
||||||
alt={shortCode}
|
|
||||||
title={shortCode}
|
|
||||||
src={filename}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Reaction extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reaction: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hovered: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
const { reaction, announcementId, addReaction, removeReaction } = this.props;
|
|
||||||
|
|
||||||
if (reaction.get('me')) {
|
|
||||||
removeReaction(announcementId, reaction.get('name'));
|
|
||||||
} else {
|
|
||||||
addReaction(announcementId, reaction.get('name'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = () => this.setState({ hovered: true });
|
|
||||||
|
|
||||||
handleMouseLeave = () => this.setState({ hovered: false });
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { reaction } = this.props;
|
|
||||||
|
|
||||||
let shortCode = reaction.get('name');
|
|
||||||
|
|
||||||
if (unicodeMapping[shortCode]) {
|
|
||||||
shortCode = unicodeMapping[shortCode].shortCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<animated.button
|
|
||||||
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
title={`:${shortCode}:`}
|
|
||||||
style={this.props.style}
|
|
||||||
// This does not use animate-parent as this component is directly rendered by React.
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
>
|
|
||||||
<span className='reactions-bar__item__emoji'>
|
|
||||||
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
|
|
||||||
</span>
|
|
||||||
<span className='reactions-bar__item__count'>
|
|
||||||
<AnimatedNumber value={reaction.get('count')} />
|
|
||||||
</span>
|
|
||||||
</animated.button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactionsBar = ({
|
|
||||||
announcementId,
|
|
||||||
reactions,
|
|
||||||
emojiMap,
|
|
||||||
addReaction,
|
|
||||||
removeReaction,
|
|
||||||
}) => {
|
|
||||||
const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
|
|
||||||
|
|
||||||
const handleEmojiPick = useCallback((emoji) => {
|
|
||||||
addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
|
|
||||||
}, [addReaction, announcementId]);
|
|
||||||
|
|
||||||
const transitions = useTransition(visibleReactions, {
|
|
||||||
from: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
enter: {
|
|
||||||
scale: 1,
|
|
||||||
},
|
|
||||||
leave: {
|
|
||||||
scale: 0,
|
|
||||||
},
|
|
||||||
keys: visibleReactions.map(x => x.get('name')),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames('reactions-bar', {
|
|
||||||
'reactions-bar--empty': visibleReactions.length === 0
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{transitions(({ scale }, reaction) => (
|
|
||||||
<Reaction
|
|
||||||
key={reaction.get('name')}
|
|
||||||
reaction={reaction}
|
|
||||||
style={{ transform: scale.to((s) => `scale(${s})`) }}
|
|
||||||
addReaction={addReaction}
|
|
||||||
removeReaction={removeReaction}
|
|
||||||
announcementId={announcementId}
|
|
||||||
emojiMap={emojiMap}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{visibleReactions.length < 8 && (
|
|
||||||
<EmojiPickerDropdown
|
|
||||||
onPickEmoji={handleEmojiPick}
|
|
||||||
button={<Icon id='plus' icon={AddIcon} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ReactionsBar.propTypes = {
|
|
||||||
announcementId: PropTypes.string.isRequired,
|
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class Announcement extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcement: ImmutablePropTypes.map.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
selected: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
unread: !this.props.announcement.get('read'),
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
const { selected, announcement } = this.props;
|
|
||||||
if (!selected && this.state.unread !== !announcement.get('read')) {
|
|
||||||
this.setState({ unread: !announcement.get('read') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcement } = this.props;
|
|
||||||
const { unread } = this.state;
|
|
||||||
const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at'));
|
|
||||||
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
|
|
||||||
const now = new Date();
|
|
||||||
const hasTimeRange = startsAt && endsAt;
|
|
||||||
const skipTime = announcement.get('all_day');
|
|
||||||
|
|
||||||
let timestamp = null;
|
|
||||||
if (hasTimeRange) {
|
|
||||||
const skipYear = startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
|
|
||||||
const skipEndDate = startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
|
|
||||||
timestamp = (
|
|
||||||
<>
|
|
||||||
<FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const publishedAt = new Date(announcement.get('published_at'));
|
|
||||||
timestamp = (
|
|
||||||
<FormattedDate value={publishedAt} year={publishedAt.getFullYear() === now.getFullYear() ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements__item'>
|
|
||||||
<strong className='announcements__item__range'>
|
|
||||||
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
|
|
||||||
<span> · {timestamp}</span>
|
|
||||||
</strong>
|
|
||||||
|
|
||||||
<Content announcement={announcement} />
|
|
||||||
|
|
||||||
<ReactionsBar
|
|
||||||
reactions={announcement.get('reactions')}
|
|
||||||
announcementId={announcement.get('id')}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{unread && <span className='announcements__item__unread' />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Announcements extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
announcements: ImmutablePropTypes.list,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
dismissAnnouncement: PropTypes.func.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
index: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
|
||||||
if (props.announcements.size > 0 && state.index >= props.announcements.size) {
|
|
||||||
return { index: props.announcements.size - 1 };
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._markAnnouncementAsRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
_markAnnouncementAsRead () {
|
|
||||||
const { dismissAnnouncement, announcements } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
const announcement = announcements.get(announcements.size - 1 - index);
|
|
||||||
if (!announcement.get('read')) dismissAnnouncement(announcement.get('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeIndex = index => {
|
|
||||||
this.setState({ index: index % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNextClick = () => {
|
|
||||||
this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrevClick = () => {
|
|
||||||
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { announcements, intl } = this.props;
|
|
||||||
const { index } = this.state;
|
|
||||||
|
|
||||||
if (announcements.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='announcements'>
|
|
||||||
<img className='announcements__mastodon' alt='' draggable='false' src={mascot || elephantUIPlane} />
|
|
||||||
|
|
||||||
<div className='announcements__container'>
|
|
||||||
<ReactSwipeableViews animateHeight animateTransitions={!reduceMotion} index={index} onChangeIndex={this.handleChangeIndex}>
|
|
||||||
{announcements.map((announcement, idx) => (
|
|
||||||
<Announcement
|
|
||||||
key={announcement.get('id')}
|
|
||||||
announcement={announcement}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
intl={intl}
|
|
||||||
selected={index === idx}
|
|
||||||
disabled={disableSwiping}
|
|
||||||
/>
|
|
||||||
)).reverse()}
|
|
||||||
</ReactSwipeableViews>
|
|
||||||
|
|
||||||
{announcements.size > 1 && (
|
|
||||||
<div className='announcements__pagination'>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.previous)} icon='chevron-left' iconComponent={ChevronLeftIcon} onClick={this.handlePrevClick} size={13} />
|
|
||||||
<span>{index + 1} / {announcements.size}</span>
|
|
||||||
<IconButton disabled={announcements.size === 1} title={intl.formatMessage(messages.next)} icon='chevron-right' iconComponent={ChevronRightIcon} onClick={this.handleNextClick} size={13} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(Announcements);
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
|
||||||
import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements';
|
|
||||||
|
|
||||||
import Announcements from '../components/announcements';
|
|
||||||
|
|
||||||
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
announcements: state.getIn(['announcements', 'items']),
|
|
||||||
emojiMap: customEmojiMap(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
dismissAnnouncement: id => dispatch(dismissAnnouncement(id)),
|
|
||||||
addReaction: (id, name) => dispatch(addReaction(id, name)),
|
|
||||||
removeReaction: (id, name) => dispatch(removeReaction(id, name)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Announcements);
|
|
||||||
@ -10,10 +10,8 @@ import ReactSwipeableViews from 'react-swipeable-views';
|
|||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
import { IconButton } from '@/mastodon/components/icon_button';
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
import LegacyAnnouncements from '@/mastodon/features/getting_started/containers/announcements_container';
|
|
||||||
import { mascot, reduceMotion } from '@/mastodon/initial_state';
|
import { mascot, reduceMotion } from '@/mastodon/initial_state';
|
||||||
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
import { createAppSelector, useAppSelector } from '@/mastodon/store';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
|
||||||
@ -32,7 +30,7 @@ const announcementSelector = createAppSelector(
|
|||||||
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
(announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ModernAnnouncements: FC = () => {
|
export const Announcements: FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const announcements = useAppSelector(announcementSelector);
|
const announcements = useAppSelector(announcementSelector);
|
||||||
@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Announcements = isModernEmojiEnabled()
|
|
||||||
? ModernAnnouncements
|
|
||||||
: LegacyAnnouncements;
|
|
||||||
|
|||||||
@ -61,6 +61,10 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
explore: { id: 'explore.title', defaultMessage: 'Trending' },
|
explore: { id: 'explore.title', defaultMessage: 'Trending' },
|
||||||
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' },
|
||||||
|
firehose_singular: {
|
||||||
|
id: 'column.firehose_singular',
|
||||||
|
defaultMessage: 'Live feed',
|
||||||
|
},
|
||||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' },
|
||||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
@ -275,7 +279,12 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
|||||||
icon='globe'
|
icon='globe'
|
||||||
iconComponent={PublicIcon}
|
iconComponent={PublicIcon}
|
||||||
isActive={isFirehoseActive}
|
isActive={isFirehoseActive}
|
||||||
text={intl.formatMessage(messages.firehose)}
|
text={intl.formatMessage(
|
||||||
|
canViewFeed(signedIn, permissions, localLiveFeedAccess) &&
|
||||||
|
canViewFeed(signedIn, permissions, remoteLiveFeedAccess)
|
||||||
|
? messages.firehose
|
||||||
|
: messages.firehose_singular,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,17 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
import type { History } from 'history';
|
|
||||||
|
|
||||||
import type { ApiMentionJSON } from '@/mastodon/api_types/statuses';
|
|
||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
||||||
import type { Status } from '@/mastodon/models/status';
|
import type { Status } from '@/mastodon/models/status';
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
|
||||||
|
|
||||||
import type { Mention } from './embedded_status';
|
import type { Mention } from './embedded_status';
|
||||||
|
|
||||||
const handleMentionClick = (
|
|
||||||
history: History,
|
|
||||||
mention: ApiMentionJSON,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/@${mention.acct}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHashtagClick = (
|
|
||||||
history: History,
|
|
||||||
hashtag: string,
|
|
||||||
e: MouseEvent,
|
|
||||||
) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmbeddedStatusContent: React.FC<{
|
export const EmbeddedStatusContent: React.FC<{
|
||||||
status: Status;
|
status: Status;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ status, className }) => {
|
}> = ({ status, className }) => {
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const mentions = useMemo(
|
const mentions = useMemo(
|
||||||
() => (status.get('mentions') as List<Mention>).toJS(),
|
() => (status.get('mentions') as List<Mention>).toJS(),
|
||||||
[status],
|
[status],
|
||||||
@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{
|
|||||||
hrefToMention,
|
hrefToMention,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleContentRef = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (!node || isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = node.querySelectorAll<HTMLAnchorElement>('a');
|
|
||||||
|
|
||||||
for (const link of links) {
|
|
||||||
if (link.classList.contains('status-link')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
link.classList.add('status-link');
|
|
||||||
|
|
||||||
const mention = mentions.find((item) => link.href === item.url);
|
|
||||||
|
|
||||||
if (mention) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleMentionClick.bind(null, history, mention),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('title', `@${mention.acct}`);
|
|
||||||
link.setAttribute('href', `/@${mention.acct}`);
|
|
||||||
} else if (
|
|
||||||
link.textContent.startsWith('#') ||
|
|
||||||
link.previousSibling?.textContent?.endsWith('#')
|
|
||||||
) {
|
|
||||||
link.addEventListener(
|
|
||||||
'click',
|
|
||||||
handleHashtagClick.bind(null, history, link.text),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
|
||||||
} else {
|
|
||||||
link.setAttribute('title', link.href);
|
|
||||||
link.classList.add('unhandled-link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[mentions, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
className={className}
|
className={className}
|
||||||
ref={handleContentRef}
|
|
||||||
lang={status.get('language') as string}
|
lang={status.get('language') as string}
|
||||||
htmlString={status.get('contentHtml') as string}
|
htmlString={status.get('contentHtml') as string}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import { IconButton } from 'mastodon/components/icon_button';
|
|||||||
import InlineAccount from 'mastodon/components/inline_account';
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
import MediaAttachments from 'mastodon/components/media_attachments';
|
import MediaAttachments from 'mastodon/components/media_attachments';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
||||||
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||||
|
|
||||||
@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
const { index, versions, language, onClose } = this.props;
|
const { index, versions, language, onClose } = this.props;
|
||||||
const currentVersion = versions.get(index);
|
const currentVersion = versions.get(index);
|
||||||
|
|
||||||
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
|
const content = currentVersion.get('content');
|
||||||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
|
const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text'));
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const content = emojify(currentVersion.get('content'), emojiMap);
|
|
||||||
const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap);
|
|
||||||
|
|
||||||
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
|
||||||
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
|
||||||
@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent {
|
|||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
as="span"
|
as="span"
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
htmlString={emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)}
|
htmlString={escapeTextContentForBrowser(option.get('title'))}
|
||||||
lang={language}
|
lang={language}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -22,12 +22,11 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
|
|||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
import { handleAnimateGif } from '../emoji/handlers';
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, autoPlayGif } from '../../initial_state';
|
import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@ -382,11 +381,6 @@ class UI extends PureComponent {
|
|||||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
|
||||||
if (!autoPlayGif) {
|
|
||||||
window.addEventListener('mouseover', handleAnimateGif, { passive: true });
|
|
||||||
window.addEventListener('mouseout', handleAnimateGif, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
document.addEventListener('drop', this.handleDrop, false);
|
document.addEventListener('drop', this.handleDrop, false);
|
||||||
@ -412,8 +406,6 @@ class UI extends PureComponent {
|
|||||||
window.removeEventListener('blur', this.handleWindowBlur);
|
window.removeEventListener('blur', this.handleWindowBlur);
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
window.removeEventListener('mouseover', handleAnimateGif);
|
|
||||||
window.removeEventListener('mouseout', handleAnimateGif);
|
|
||||||
|
|
||||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { openURL } from 'mastodon/actions/search';
|
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.classList.contains('mention') &&
|
|
||||||
!element.classList.contains('hashtag');
|
|
||||||
|
|
||||||
const isHashtagClick = (element: HTMLAnchorElement) =>
|
|
||||||
element.textContent.startsWith('#') ||
|
|
||||||
element.previousSibling?.textContent?.endsWith('#');
|
|
||||||
|
|
||||||
export const useLinks = (skipHashtags?: boolean) => {
|
|
||||||
const history = useHistory();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const handleHashtagClick = useCallback(
|
|
||||||
(element: HTMLAnchorElement) => {
|
|
||||||
const { textContent } = element;
|
|
||||||
|
|
||||||
if (!textContent) return;
|
|
||||||
|
|
||||||
history.push(`/tags/${textContent.replace(/^#/, '')}`);
|
|
||||||
},
|
|
||||||
[history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMentionClick = useCallback(
|
|
||||||
async (element: HTMLAnchorElement) => {
|
|
||||||
const result = await dispatch(openURL({ url: element.href }));
|
|
||||||
|
|
||||||
if (isFulfilled(result)) {
|
|
||||||
if (result.payload.accounts[0]) {
|
|
||||||
history.push(`/@${result.payload.accounts[0].acct}`);
|
|
||||||
} else if (result.payload.statuses[0]) {
|
|
||||||
history.push(
|
|
||||||
`/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
} else if (isRejected(result)) {
|
|
||||||
window.location.href = element.href;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, history],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClick = useCallback(
|
|
||||||
(e: React.MouseEvent) => {
|
|
||||||
// Exit early if modern emoji is enabled, as this is handled by HandledLink.
|
|
||||||
if (isModernEmojiEnabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = (e.target as HTMLElement).closest('a');
|
|
||||||
|
|
||||||
if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMentionClick(target)) {
|
|
||||||
e.preventDefault();
|
|
||||||
void handleMentionClick(target);
|
|
||||||
} else if (isHashtagClick(target) && !skipHashtags) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleHashtagClick(target);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[skipHashtags, handleMentionClick, handleHashtagClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
return handleClick;
|
|
||||||
};
|
|
||||||
@ -28,6 +28,7 @@
|
|||||||
"account.disable_notifications": "Спиране на известяване при публикуване от @{name}",
|
"account.disable_notifications": "Спиране на известяване при публикуване от @{name}",
|
||||||
"account.domain_blocking": "Блокиране на домейн",
|
"account.domain_blocking": "Блокиране на домейн",
|
||||||
"account.edit_profile": "Редактиране на профила",
|
"account.edit_profile": "Редактиране на профила",
|
||||||
|
"account.edit_profile_short": "Редактиране",
|
||||||
"account.enable_notifications": "Известяване при публикуване от @{name}",
|
"account.enable_notifications": "Известяване при публикуване от @{name}",
|
||||||
"account.endorse": "Представи в профила",
|
"account.endorse": "Представи в профила",
|
||||||
"account.familiar_followers_many": "Последвано от {name1}, {name2}, и {othersCount, plural, one {един друг, когото познавате} other {# други, които познавате}}",
|
"account.familiar_followers_many": "Последвано от {name1}, {name2}, и {othersCount, plural, one {един друг, когото познавате} other {# други, които познавате}}",
|
||||||
@ -40,6 +41,9 @@
|
|||||||
"account.featured_tags.last_status_never": "Няма публикации",
|
"account.featured_tags.last_status_never": "Няма публикации",
|
||||||
"account.follow": "Последване",
|
"account.follow": "Последване",
|
||||||
"account.follow_back": "Последване взаимно",
|
"account.follow_back": "Последване взаимно",
|
||||||
|
"account.follow_request_cancel": "Отказване на заявката",
|
||||||
|
"account.follow_request_cancel_short": "Отказ",
|
||||||
|
"account.follow_request_short": "Заявка",
|
||||||
"account.followers": "Последователи",
|
"account.followers": "Последователи",
|
||||||
"account.followers.empty": "Още никой не следва потребителя.",
|
"account.followers.empty": "Още никой не следва потребителя.",
|
||||||
"account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}",
|
"account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}",
|
||||||
@ -238,6 +242,9 @@
|
|||||||
"confirmations.missing_alt_text.secondary": "Все пак да се публикува",
|
"confirmations.missing_alt_text.secondary": "Все пак да се публикува",
|
||||||
"confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?",
|
"confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?",
|
||||||
"confirmations.mute.confirm": "Заглушаване",
|
"confirmations.mute.confirm": "Заглушаване",
|
||||||
|
"confirmations.quiet_post_quote_info.dismiss": "Без друго напомняне",
|
||||||
|
"confirmations.quiet_post_quote_info.got_it": "Схванах",
|
||||||
|
"confirmations.quiet_post_quote_info.title": "Цитиране на публикации за тиха публика",
|
||||||
"confirmations.redraft.confirm": "Изтриване и преработване",
|
"confirmations.redraft.confirm": "Изтриване и преработване",
|
||||||
"confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.",
|
"confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.",
|
||||||
"confirmations.redraft.title": "Изтривате и преработвате ли публикацията?",
|
"confirmations.redraft.title": "Изтривате и преработвате ли публикацията?",
|
||||||
@ -247,7 +254,11 @@
|
|||||||
"confirmations.revoke_quote.confirm": "Премахване на публикация",
|
"confirmations.revoke_quote.confirm": "Премахване на публикация",
|
||||||
"confirmations.revoke_quote.message": "Действието е неотменимо.",
|
"confirmations.revoke_quote.message": "Действието е неотменимо.",
|
||||||
"confirmations.revoke_quote.title": "Премахвате ли публикацията?",
|
"confirmations.revoke_quote.title": "Премахвате ли публикацията?",
|
||||||
|
"confirmations.unblock.confirm": "Отблокиране",
|
||||||
|
"confirmations.unblock.title": "Отблокирате ли @{name}?",
|
||||||
"confirmations.unfollow.confirm": "Без следване",
|
"confirmations.unfollow.confirm": "Без следване",
|
||||||
|
"confirmations.unfollow.title": "Спирате ли следване на {name}?",
|
||||||
|
"confirmations.withdraw_request.confirm": "Оттегляне на заявката",
|
||||||
"content_warning.hide": "Скриване на публ.",
|
"content_warning.hide": "Скриване на публ.",
|
||||||
"content_warning.show": "Нека се покаже",
|
"content_warning.show": "Нека се покаже",
|
||||||
"content_warning.show_more": "Показване на още",
|
"content_warning.show_more": "Показване на още",
|
||||||
@ -442,10 +453,12 @@
|
|||||||
"ignore_notifications_modal.private_mentions_title": "Пренебрегвате ли известия от непоискани лични споменавания?",
|
"ignore_notifications_modal.private_mentions_title": "Пренебрегвате ли известия от непоискани лични споменавания?",
|
||||||
"info_button.label": "Помощ",
|
"info_button.label": "Помощ",
|
||||||
"info_button.what_is_alt_text": "<h1>Какво е алтернативен текст?</h1> <p>Алтернативният текст осигурява описания на изображение за хора със зрителни увреждания, връзки с ниска честотна лента или търсещите допълнителен контекст.</p> <p>Може да подобрите достъпността и разбираемостта за всеки, пишейки ясен, кратък и обективен алтернативен текст.</p> <ul> <li>Уловете важните елементи</li> <li>Обобщете текста в образите</li> <li>Употребявайте правилна структура на изречението</li> <li>Избягвайте излишна информация</li> <li>Съсредоточете се върху тенденциите и ключови констатации в сложни онагледявания (като диаграми и карти)</li> </ul>",
|
"info_button.what_is_alt_text": "<h1>Какво е алтернативен текст?</h1> <p>Алтернативният текст осигурява описания на изображение за хора със зрителни увреждания, връзки с ниска честотна лента или търсещите допълнителен контекст.</p> <p>Може да подобрите достъпността и разбираемостта за всеки, пишейки ясен, кратък и обективен алтернативен текст.</p> <ul> <li>Уловете важните елементи</li> <li>Обобщете текста в образите</li> <li>Употребявайте правилна структура на изречението</li> <li>Избягвайте излишна информация</li> <li>Съсредоточете се върху тенденциите и ключови констатации в сложни онагледявания (като диаграми и карти)</li> </ul>",
|
||||||
|
"interaction_modal.action": "Трябва да влезете с акаунта си, в който и да е сървър на Mastodon, когото използвате, за да взаимодействате с публикация на {name}.",
|
||||||
"interaction_modal.go": "Напред",
|
"interaction_modal.go": "Напред",
|
||||||
"interaction_modal.no_account_yet": "Още ли нямате акаунт?",
|
"interaction_modal.no_account_yet": "Още ли нямате акаунт?",
|
||||||
"interaction_modal.on_another_server": "На различен сървър",
|
"interaction_modal.on_another_server": "На различен сървър",
|
||||||
"interaction_modal.on_this_server": "На този сървър",
|
"interaction_modal.on_this_server": "На този сървър",
|
||||||
|
"interaction_modal.title": "Влезте, за да продължите",
|
||||||
"interaction_modal.username_prompt": "Напр. {example}",
|
"interaction_modal.username_prompt": "Напр. {example}",
|
||||||
"intervals.full.days": "{number, plural, one {# ден} other {# дни}}",
|
"intervals.full.days": "{number, plural, one {# ден} other {# дни}}",
|
||||||
"intervals.full.hours": "{number, plural, one {# час} other {# часа}}",
|
"intervals.full.hours": "{number, plural, one {# час} other {# часа}}",
|
||||||
@ -596,6 +609,7 @@
|
|||||||
"notification.moderation_warning.action_suspend": "Вашият акаунт е спрян.",
|
"notification.moderation_warning.action_suspend": "Вашият акаунт е спрян.",
|
||||||
"notification.own_poll": "Анкетата ви приключи",
|
"notification.own_poll": "Анкетата ви приключи",
|
||||||
"notification.poll": "Анкета, в която гласувахте, приключи",
|
"notification.poll": "Анкета, в която гласувахте, приключи",
|
||||||
|
"notification.quoted_update": "{name} редактира публикация, която цитирахте",
|
||||||
"notification.reblog": "{name} подсили ваша публикация",
|
"notification.reblog": "{name} подсили ваша публикация",
|
||||||
"notification.reblog.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> подсилиха ваша публикация",
|
"notification.reblog.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> подсилиха ваша публикация",
|
||||||
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
|
"notification.relationships_severance_event": "Изгуби се връзката с {name}",
|
||||||
@ -715,10 +729,17 @@
|
|||||||
"privacy.private.short": "Последователи",
|
"privacy.private.short": "Последователи",
|
||||||
"privacy.public.long": "Всеки във и извън Mastodon",
|
"privacy.public.long": "Всеки във и извън Mastodon",
|
||||||
"privacy.public.short": "Публично",
|
"privacy.public.short": "Публично",
|
||||||
|
"privacy.quote.anyone": "{visibility}, всеки може да цитира",
|
||||||
|
"privacy.quote.disabled": "{visibility}, цитатите са изключени",
|
||||||
|
"privacy.quote.limited": "{visibility}, цитатите са ограничени",
|
||||||
"privacy.unlisted.additional": "Това действие е точно като публичното, с изключение на това, че публикацията няма да се появява в каналите на живо, хаштаговете, разглеждането или търсенето в Mastodon, дори ако сте избрали да се публично видими на ниво акаунт.",
|
"privacy.unlisted.additional": "Това действие е точно като публичното, с изключение на това, че публикацията няма да се появява в каналите на живо, хаштаговете, разглеждането или търсенето в Mastodon, дори ако сте избрали да се публично видими на ниво акаунт.",
|
||||||
"privacy.unlisted.short": "Тиха публика",
|
"privacy.unlisted.short": "Тиха публика",
|
||||||
"privacy_policy.last_updated": "Последно осъвременяване на {date}",
|
"privacy_policy.last_updated": "Последно осъвременяване на {date}",
|
||||||
"privacy_policy.title": "Политика за поверителност",
|
"privacy_policy.title": "Политика за поверителност",
|
||||||
|
"quote_error.edit": "Не може да се добавят цитати, редайтирайки публикация.",
|
||||||
|
"quote_error.poll": "Не може да се цитира при анкетиране.",
|
||||||
|
"quote_error.unauthorized": "Нямате право да цитирате тази публикация.",
|
||||||
|
"quote_error.upload": "Цитирането не е позволено с мултимедийни прикачвания.",
|
||||||
"recommended": "Препоръчано",
|
"recommended": "Препоръчано",
|
||||||
"refresh": "Опресняване",
|
"refresh": "Опресняване",
|
||||||
"regeneration_indicator.please_stand_by": "Изчакайте.",
|
"regeneration_indicator.please_stand_by": "Изчакайте.",
|
||||||
@ -734,6 +755,8 @@
|
|||||||
"relative_time.minutes": "{number}м.",
|
"relative_time.minutes": "{number}м.",
|
||||||
"relative_time.seconds": "{number}с.",
|
"relative_time.seconds": "{number}с.",
|
||||||
"relative_time.today": "днес",
|
"relative_time.today": "днес",
|
||||||
|
"remove_quote_hint.button_label": "Схванах",
|
||||||
|
"remove_quote_hint.message": "Може да го направите от менюто възможности {icon}.",
|
||||||
"reply_indicator.attachments": "{count, plural, one {# прикаване} other {# прикачвания}}",
|
"reply_indicator.attachments": "{count, plural, one {# прикаване} other {# прикачвания}}",
|
||||||
"reply_indicator.cancel": "Отказ",
|
"reply_indicator.cancel": "Отказ",
|
||||||
"reply_indicator.poll": "Анкета",
|
"reply_indicator.poll": "Анкета",
|
||||||
@ -825,13 +848,22 @@
|
|||||||
"status.admin_account": "Отваряне на интерфейс за модериране за @{name}",
|
"status.admin_account": "Отваряне на интерфейс за модериране за @{name}",
|
||||||
"status.admin_domain": "Отваряне на модериращия интерфейс за {domain}",
|
"status.admin_domain": "Отваряне на модериращия интерфейс за {domain}",
|
||||||
"status.admin_status": "Отваряне на публикацията в модериращия интерфейс",
|
"status.admin_status": "Отваряне на публикацията в модериращия интерфейс",
|
||||||
|
"status.all_disabled": "Подсилването и цитатите са изключени",
|
||||||
"status.block": "Блокиране на @{name}",
|
"status.block": "Блокиране на @{name}",
|
||||||
"status.bookmark": "Отмятане",
|
"status.bookmark": "Отмятане",
|
||||||
"status.cancel_reblog_private": "Край на подсилването",
|
"status.cancel_reblog_private": "Край на подсилването",
|
||||||
|
"status.cannot_quote": "Не е позволено да цитирате тази публикация",
|
||||||
"status.cannot_reblog": "Публикацията не може да се подсилва",
|
"status.cannot_reblog": "Публикацията не може да се подсилва",
|
||||||
|
"status.context.loading": "Зареждане на още отговори",
|
||||||
|
"status.context.loading_error": "Не можаха да се заредят нови отговори",
|
||||||
|
"status.context.loading_success": "Новите отговори заредени",
|
||||||
|
"status.context.more_replies_found": "Още намерени отговори",
|
||||||
|
"status.context.retry": "Друг опит",
|
||||||
|
"status.context.show": "Показване",
|
||||||
"status.continued_thread": "Продължена нишка",
|
"status.continued_thread": "Продължена нишка",
|
||||||
"status.copy": "Копиране на връзката към публикация",
|
"status.copy": "Копиране на връзката към публикация",
|
||||||
"status.delete": "Изтриване",
|
"status.delete": "Изтриване",
|
||||||
|
"status.delete.success": "Публикацията е изтрита",
|
||||||
"status.detailed_status": "Подробен изглед на разговора",
|
"status.detailed_status": "Подробен изглед на разговора",
|
||||||
"status.direct": "Частно споменаване на @{name}",
|
"status.direct": "Частно споменаване на @{name}",
|
||||||
"status.direct_indicator": "Частно споменаване",
|
"status.direct_indicator": "Частно споменаване",
|
||||||
@ -855,23 +887,32 @@
|
|||||||
"status.open": "Разширяване на публикацията",
|
"status.open": "Разширяване на публикацията",
|
||||||
"status.pin": "Закачане в профила",
|
"status.pin": "Закачане в профила",
|
||||||
"status.quote_error.filtered": "Скрито поради един от филтрите ви",
|
"status.quote_error.filtered": "Скрито поради един от филтрите ви",
|
||||||
|
"status.quote_error.limited_account_hint.title": "Този акаунт е бил скрит от модераторите на {domain}.",
|
||||||
"status.quote_error.not_available": "Неналична публикация",
|
"status.quote_error.not_available": "Неналична публикация",
|
||||||
"status.quote_error.pending_approval": "Публикацията чака одобрение",
|
"status.quote_error.pending_approval": "Публикацията чака одобрение",
|
||||||
|
"status.quote_error.revoked": "Премахната публикация от автора",
|
||||||
|
"status.quote_followers_only": "Само последователи могат да цитират тази публикация",
|
||||||
|
"status.quote_manual_review": "Авторът ще преглежда ръчно",
|
||||||
"status.quote_policy_change": "Промяна кой може да цитира",
|
"status.quote_policy_change": "Промяна кой може да цитира",
|
||||||
"status.quote_post_author": "Цитирах публикация от @{name}",
|
"status.quote_post_author": "Цитирах публикация от @{name}",
|
||||||
|
"status.quote_private": "Частните публикации не може да се цитират",
|
||||||
"status.read_more": "Още за четене",
|
"status.read_more": "Още за четене",
|
||||||
"status.reblog": "Подсилване",
|
"status.reblog": "Подсилване",
|
||||||
|
"status.reblog_or_quote": "Подсилване или цитиране",
|
||||||
|
"status.reblog_private": "Споделете пак с последователите си",
|
||||||
"status.reblogged_by": "{name} подсили",
|
"status.reblogged_by": "{name} подсили",
|
||||||
"status.reblogs": "{count, plural, one {подсилване} other {подсилвания}}",
|
"status.reblogs": "{count, plural, one {подсилване} other {подсилвания}}",
|
||||||
"status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.",
|
"status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.",
|
||||||
"status.redraft": "Изтриване и преработване",
|
"status.redraft": "Изтриване и преработване",
|
||||||
"status.remove_bookmark": "Премахване на отметката",
|
"status.remove_bookmark": "Премахване на отметката",
|
||||||
"status.remove_favourite": "Премахване от любими",
|
"status.remove_favourite": "Премахване от любими",
|
||||||
|
"status.remove_quote": "Премахване",
|
||||||
"status.replied_in_thread": "Отговорено в нишката",
|
"status.replied_in_thread": "Отговорено в нишката",
|
||||||
"status.replied_to": "В отговор до {name}",
|
"status.replied_to": "В отговор до {name}",
|
||||||
"status.reply": "Отговор",
|
"status.reply": "Отговор",
|
||||||
"status.replyAll": "Отговор на нишка",
|
"status.replyAll": "Отговор на нишка",
|
||||||
"status.report": "Докладване на @{name}",
|
"status.report": "Докладване на @{name}",
|
||||||
|
"status.request_quote": "Заявка за цитиране",
|
||||||
"status.revoke_quote": "Премахване на моя публикация от публикацията на @{name}",
|
"status.revoke_quote": "Премахване на моя публикация от публикацията на @{name}",
|
||||||
"status.sensitive_warning": "Деликатно съдържание",
|
"status.sensitive_warning": "Деликатно съдържание",
|
||||||
"status.share": "Споделяне",
|
"status.share": "Споделяне",
|
||||||
@ -910,6 +951,7 @@
|
|||||||
"upload_button.label": "Добавете файл с образ, видео или звук",
|
"upload_button.label": "Добавете файл с образ, видео или звук",
|
||||||
"upload_error.limit": "Превишено ограничението за качване на файлове.",
|
"upload_error.limit": "Превишено ограничението за качване на файлове.",
|
||||||
"upload_error.poll": "Качването на файлове не е позволено с анкети.",
|
"upload_error.poll": "Качването на файлове не е позволено с анкети.",
|
||||||
|
"upload_error.quote": "Цитирайки, не може да качвате файл.",
|
||||||
"upload_form.drag_and_drop.instructions": "Натиснете интервал или enter, за да подберете мултимедийно прикачване. Провлачвайки, ползвайте клавишите със стрелки, за да премествате мултимедията във всяка дадена посока. Натиснете пак интервал или enter, за да се стовари мултимедийното прикачване в новото си положение или натиснете Esc за отмяна.",
|
"upload_form.drag_and_drop.instructions": "Натиснете интервал или enter, за да подберете мултимедийно прикачване. Провлачвайки, ползвайте клавишите със стрелки, за да премествате мултимедията във всяка дадена посока. Натиснете пак интервал или enter, за да се стовари мултимедийното прикачване в новото си положение или натиснете Esc за отмяна.",
|
||||||
"upload_form.drag_and_drop.on_drag_cancel": "Провлачването е отменено. Мултимедийното прикачване {item} е спуснато.",
|
"upload_form.drag_and_drop.on_drag_cancel": "Провлачването е отменено. Мултимедийното прикачване {item} е спуснато.",
|
||||||
"upload_form.drag_and_drop.on_drag_end": "Мултимедийното прикачване {item} е спуснато.",
|
"upload_form.drag_and_drop.on_drag_end": "Мултимедийното прикачване {item} е спуснато.",
|
||||||
@ -935,6 +977,11 @@
|
|||||||
"video.volume_up": "Увеличаване на звука",
|
"video.volume_up": "Увеличаване на звука",
|
||||||
"visibility_modal.button_title": "Задаване на видимост",
|
"visibility_modal.button_title": "Задаване на видимост",
|
||||||
"visibility_modal.header": "Видимост и взаимодействие",
|
"visibility_modal.header": "Видимост и взаимодействие",
|
||||||
|
"visibility_modal.helper.privacy_editing": "Видимостта не може да се променя след публикуване на публикацията.",
|
||||||
|
"visibility_modal.privacy_label": "Видимост",
|
||||||
"visibility_modal.quote_followers": "Само последователи",
|
"visibility_modal.quote_followers": "Само последователи",
|
||||||
"visibility_modal.quote_public": "Някой"
|
"visibility_modal.quote_label": "Кой може да цитира",
|
||||||
|
"visibility_modal.quote_nobody": "Само аз",
|
||||||
|
"visibility_modal.quote_public": "Някой",
|
||||||
|
"visibility_modal.save": "Запазване"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,6 +173,8 @@
|
|||||||
"column.edit_list": "Edit list",
|
"column.edit_list": "Edit list",
|
||||||
"column.favourites": "Favorites",
|
"column.favourites": "Favorites",
|
||||||
"column.firehose": "Live feeds",
|
"column.firehose": "Live feeds",
|
||||||
|
"column.firehose_local": "Live feed for this server",
|
||||||
|
"column.firehose_singular": "Live feed",
|
||||||
"column.follow_requests": "Follow requests",
|
"column.follow_requests": "Follow requests",
|
||||||
"column.home": "Home",
|
"column.home": "Home",
|
||||||
"column.list_members": "Manage list members",
|
"column.list_members": "Manage list members",
|
||||||
|
|||||||
@ -333,6 +333,7 @@
|
|||||||
"empty_column.bookmarked_statuses": "Järjehoidjatesse pole veel lisatud postitusi. Kui lisad mõne, näed neid siin.",
|
"empty_column.bookmarked_statuses": "Järjehoidjatesse pole veel lisatud postitusi. Kui lisad mõne, näed neid siin.",
|
||||||
"empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema ajada!",
|
"empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema ajada!",
|
||||||
"empty_column.direct": "Sul pole veel ühtegi privaatset mainimist. Kui saadad või saad mõne, ilmuvad need siin.",
|
"empty_column.direct": "Sul pole veel ühtegi privaatset mainimist. Kui saadad või saad mõne, ilmuvad need siin.",
|
||||||
|
"empty_column.disabled_feed": "See infovoog on serveri peakasutajate poolt välja lülitatud.",
|
||||||
"empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.",
|
"empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.",
|
||||||
"empty_column.explore_statuses": "Praegu pole ühtegi trendi. Tule hiljem tagasi!",
|
"empty_column.explore_statuses": "Praegu pole ühtegi trendi. Tule hiljem tagasi!",
|
||||||
"empty_column.favourited_statuses": "Pole veel lemmikpostitusi. Kui märgid mõne, näed neid siin.",
|
"empty_column.favourited_statuses": "Pole veel lemmikpostitusi. Kui märgid mõne, näed neid siin.",
|
||||||
|
|||||||
@ -333,6 +333,7 @@
|
|||||||
"empty_column.bookmarked_statuses": "Tú hevur enn einki goymt uppslag. Tú tú goymir eitt uppslag, kemur tað her.",
|
"empty_column.bookmarked_statuses": "Tú hevur enn einki goymt uppslag. Tú tú goymir eitt uppslag, kemur tað her.",
|
||||||
"empty_column.community": "Lokala tíðarlinjan er tóm. Skriva okkurt alment fyri at fáa boltin á rull!",
|
"empty_column.community": "Lokala tíðarlinjan er tóm. Skriva okkurt alment fyri at fáa boltin á rull!",
|
||||||
"empty_column.direct": "Tú hevur ongar privatar umrøður enn. Tá tú sendir ella móttekur eina privata umrøðu, so verður hon sjónlig her.",
|
"empty_column.direct": "Tú hevur ongar privatar umrøður enn. Tá tú sendir ella móttekur eina privata umrøðu, so verður hon sjónlig her.",
|
||||||
|
"empty_column.disabled_feed": "Hendan rásin er gjørd óvirkin av ambætaraumsitarunum hjá tær.",
|
||||||
"empty_column.domain_blocks": "Enn eru eingi blokeraði domenir.",
|
"empty_column.domain_blocks": "Enn eru eingi blokeraði domenir.",
|
||||||
"empty_column.explore_statuses": "Einki rák er beint nú. Royn aftur seinni!",
|
"empty_column.explore_statuses": "Einki rák er beint nú. Royn aftur seinni!",
|
||||||
"empty_column.favourited_statuses": "Tú hevur ongar yndispostar enn. Tá tú gevur einum posti yndismerki, so sært tú hann her.",
|
"empty_column.favourited_statuses": "Tú hevur ongar yndispostar enn. Tá tú gevur einum posti yndismerki, so sært tú hann her.",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user