From c5692d2f2f4beede8b8c158954d3ab86df3f3e98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:59:42 +0100 Subject: [PATCH 01/25] Update dependency prom-client to v15.1.1 (#29764) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a2258375b2..268aa24488 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14023,12 +14023,12 @@ __metadata: linkType: hard "prom-client@npm:^15.0.0": - version: 15.1.0 - resolution: "prom-client@npm:15.1.0" + version: 15.1.1 + resolution: "prom-client@npm:15.1.1" dependencies: "@opentelemetry/api": "npm:^1.4.0" tdigest: "npm:^0.1.1" - checksum: 10c0/c10781adbf49225298e44da5396a51a0bd4d0cddc3c7e237ba50e888e12ead26a8f98261f362a442f1bbcdaddd6e7302d5675b37beac67ea9b6f82e4d39fb3cc + checksum: 10c0/b3e6a58fc0ef87cf5b0badf06d7d79c24ac93ba47cccfaad95faeba79824c6a7724d74a257e7268d691245c847173818c16c8153054cccf16b8f033c37c74129 languageName: node linkType: hard From 1d0a43f6a353717ff6e02ab17276228c9cf452a4 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 27 Mar 2024 05:59:45 -0400 Subject: [PATCH 02/25] Use composable query in `Status.not_domain_blocked_by_account` scope (#29766) --- app/models/status.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/status.rb b/app/models/status.rb index c8effd3da7..ae53513ee6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -110,7 +110,7 @@ class Status < ApplicationRecord scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } - scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) } + scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).merge(Account.not_domain_blocked_by_account(account)) } scope :tagged_with_all, lambda { |tag_ids| Array(tag_ids).map(&:to_i).reduce(self) do |result, id| result.where(<<~SQL.squish, tag_id: id) From 86f999c1f2db7186c6d43670f78d05ebd9ccc042 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:59:50 +0100 Subject: [PATCH 03/25] Update dependency json-schema to v4.3.0 (#29769) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ad70d82740..4d36cf4e9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -373,7 +373,7 @@ GEM json-ld-preloaded (3.3.0) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (4.2.0) + json-schema (4.3.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) jwt (2.7.1) From d49343ed11b3c08c4e81ecd70e17e01bd30154a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:59:54 +0100 Subject: [PATCH 04/25] Update dependency react-intl to v6.6.4 (#29771) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 268aa24488..bcdd1bd6f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14331,8 +14331,8 @@ __metadata: linkType: hard "react-intl@npm:^6.4.2": - version: 6.6.3 - resolution: "react-intl@npm:6.6.3" + version: 6.6.4 + resolution: "react-intl@npm:6.6.4" dependencies: "@formatjs/ecma402-abstract": "npm:1.18.2" "@formatjs/icu-messageformat-parser": "npm:2.7.6" @@ -14350,7 +14350,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/8a924931668f1bf6364bb41c09fdb54972b8e3372f0768a31478dc3f8a846920caa4dc04ab3950195baa6dbf58a148f43a6a221d5795be2cbb6f4f374a5921d6 + checksum: 10c0/bd874ad3a56b0198903c02c878cfa23b5ae9a44d2c74fd4f01ddb602c97a8115de8cebed00b44f7dc5fb3031e5ef0f9cfb4bdabd105b99a785352462deb106d3 languageName: node linkType: hard From 27d014a7fa4a92117e133adfd1c3e4e1c34ddc9c Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Wed, 27 Mar 2024 13:47:09 +0100 Subject: [PATCH 05/25] Rewrite markers reducer in Typescript (#27644) --- app/javascript/mastodon/actions/markers.js | 152 ---------------- app/javascript/mastodon/actions/markers.ts | 163 ++++++++++++++++++ app/javascript/mastodon/api.ts | 9 +- app/javascript/mastodon/api_types/markers.ts | 7 + app/javascript/mastodon/reducers/index.ts | 4 +- app/javascript/mastodon/reducers/markers.js | 26 --- app/javascript/mastodon/reducers/markers.ts | 18 ++ .../mastodon/reducers/notifications.js | 6 +- app/serializers/rest/marker_serializer.rb | 2 + 9 files changed, 202 insertions(+), 185 deletions(-) delete mode 100644 app/javascript/mastodon/actions/markers.js create mode 100644 app/javascript/mastodon/actions/markers.ts create mode 100644 app/javascript/mastodon/api_types/markers.ts delete mode 100644 app/javascript/mastodon/reducers/markers.js create mode 100644 app/javascript/mastodon/reducers/markers.ts diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js deleted file mode 100644 index cfc329a8b7..0000000000 --- a/app/javascript/mastodon/actions/markers.js +++ /dev/null @@ -1,152 +0,0 @@ -import { List as ImmutableList } from 'immutable'; - -import { debounce } from 'lodash'; - -import api from '../api'; -import { compareId } from '../compare_id'; - -export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; -export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; -export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; -export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS'; - -export const synchronouslySubmitMarkers = () => (dispatch, getState) => { - const accessToken = getState().getIn(['meta', 'access_token'], ''); - const params = _buildParams(getState()); - - if (Object.keys(params).length === 0 || accessToken === '') { - return; - } - - // The Fetch API allows us to perform requests that will be carried out - // after the page closes. But that only works if the `keepalive` attribute - // is supported. - if (window.fetch && 'keepalive' in new Request('')) { - fetch('/api/v1/markers', { - keepalive: true, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify(params), - }); - - return; - } else if (navigator && navigator.sendBeacon) { - // Failing that, we can use sendBeacon, but we have to encode the data as - // FormData for DoorKeeper to recognize the token. - const formData = new FormData(); - - formData.append('bearer_token', accessToken); - - for (const [id, value] of Object.entries(params)) { - formData.append(`${id}[last_read_id]`, value.last_read_id); - } - - if (navigator.sendBeacon('/api/v1/markers', formData)) { - return; - } - } - - // If neither Fetch nor sendBeacon worked, try to perform a synchronous - // request. - try { - const client = new XMLHttpRequest(); - - client.open('POST', '/api/v1/markers', false); - client.setRequestHeader('Content-Type', 'application/json'); - client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.send(JSON.stringify(params)); - } catch (e) { - // Do not make the BeforeUnload handler error out - } -}; - -const _buildParams = (state) => { - const params = {}; - - const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null); - const lastNotificationId = state.getIn(['notifications', 'lastReadId']); - - if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) { - params.home = { - last_read_id: lastHomeId, - }; - } - - if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) { - params.notifications = { - last_read_id: lastNotificationId, - }; - } - - return params; -}; - -const debouncedSubmitMarkers = debounce((dispatch, getState) => { - const accessToken = getState().getIn(['meta', 'access_token'], ''); - const params = _buildParams(getState()); - - if (Object.keys(params).length === 0 || accessToken === '') { - return; - } - - api(getState).post('/api/v1/markers', params).then(() => { - dispatch(submitMarkersSuccess(params)); - }).catch(() => {}); -}, 300000, { leading: true, trailing: true }); - -export function submitMarkersSuccess({ home, notifications }) { - return { - type: MARKERS_SUBMIT_SUCCESS, - home: (home || {}).last_read_id, - notifications: (notifications || {}).last_read_id, - }; -} - -export function submitMarkers(params = {}) { - const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); - - if (params.immediate === true) { - debouncedSubmitMarkers.flush(); - } - - return result; -} - -export const fetchMarkers = () => (dispatch, getState) => { - const params = { timeline: ['notifications'] }; - - dispatch(fetchMarkersRequest()); - - api(getState).get('/api/v1/markers', { params }).then(response => { - dispatch(fetchMarkersSuccess(response.data)); - }).catch(error => { - dispatch(fetchMarkersFail(error)); - }); -}; - -export function fetchMarkersRequest() { - return { - type: MARKERS_FETCH_REQUEST, - skipLoading: true, - }; -} - -export function fetchMarkersSuccess(markers) { - return { - type: MARKERS_FETCH_SUCCESS, - markers, - skipLoading: true, - }; -} - -export function fetchMarkersFail(error) { - return { - type: MARKERS_FETCH_FAIL, - error, - skipLoading: true, - skipAlert: true, - }; -} diff --git a/app/javascript/mastodon/actions/markers.ts b/app/javascript/mastodon/actions/markers.ts new file mode 100644 index 0000000000..4e90a9c6bd --- /dev/null +++ b/app/javascript/mastodon/actions/markers.ts @@ -0,0 +1,163 @@ +import { List as ImmutableList } from 'immutable'; + +import { debounce } from 'lodash'; + +import type { MarkerJSON } from 'mastodon/api_types/markers'; +import type { RootState } from 'mastodon/store'; +import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; + +import api, { authorizationTokenFromState } from '../api'; +import { compareId } from '../compare_id'; + +export const synchronouslySubmitMarkers = createAppAsyncThunk( + 'markers/submit', + async (_args, { getState }) => { + const accessToken = authorizationTokenFromState(getState); + const params = buildPostMarkersParams(getState()); + + if (Object.keys(params).length === 0 || !accessToken) { + return; + } + + // The Fetch API allows us to perform requests that will be carried out + // after the page closes. But that only works if the `keepalive` attribute + // is supported. + if ('fetch' in window && 'keepalive' in new Request('')) { + await fetch('/api/v1/markers', { + keepalive: true, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(params), + }); + + return; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if ('navigator' && 'sendBeacon' in navigator) { + // Failing that, we can use sendBeacon, but we have to encode the data as + // FormData for DoorKeeper to recognize the token. + const formData = new FormData(); + + formData.append('bearer_token', accessToken); + + for (const [id, value] of Object.entries(params)) { + if (value.last_read_id) + formData.append(`${id}[last_read_id]`, value.last_read_id); + } + + if (navigator.sendBeacon('/api/v1/markers', formData)) { + return; + } + } + + // If neither Fetch nor sendBeacon worked, try to perform a synchronous + // request. + try { + const client = new XMLHttpRequest(); + + client.open('POST', '/api/v1/markers', false); + client.setRequestHeader('Content-Type', 'application/json'); + client.setRequestHeader('Authorization', `Bearer ${accessToken}`); + client.send(JSON.stringify(params)); + } catch (e) { + // Do not make the BeforeUnload handler error out + } + }, +); + +interface MarkerParam { + last_read_id?: string; +} + +function getLastHomeId(state: RootState): string | undefined { + /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ + return ( + state + // @ts-expect-error state.timelines is not yet typed + .getIn(['timelines', 'home', 'items'], ImmutableList()) + // @ts-expect-error state.timelines is not yet typed + .find((item) => item !== null) + ); +} + +function getLastNotificationId(state: RootState): string | undefined { + // @ts-expect-error state.notifications is not yet typed + return state.getIn(['notifications', 'lastReadId']); +} + +const buildPostMarkersParams = (state: RootState) => { + const params = {} as { home?: MarkerParam; notifications?: MarkerParam }; + + const lastHomeId = getLastHomeId(state); + const lastNotificationId = getLastNotificationId(state); + + if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) { + params.home = { + last_read_id: lastHomeId, + }; + } + + if ( + lastNotificationId && + compareId(lastNotificationId, state.markers.notifications) > 0 + ) { + params.notifications = { + last_read_id: lastNotificationId, + }; + } + + return params; +}; + +export const submitMarkersAction = createAppAsyncThunk<{ + home: string | undefined; + notifications: string | undefined; +}>('markers/submitAction', async (_args, { getState }) => { + const accessToken = authorizationTokenFromState(getState); + const params = buildPostMarkersParams(getState()); + + if (Object.keys(params).length === 0 || accessToken === '') { + return { home: undefined, notifications: undefined }; + } + + await api(getState).post('/api/v1/markers', params); + + return { + home: params.home?.last_read_id, + notifications: params.notifications?.last_read_id, + }; +}); + +const debouncedSubmitMarkers = debounce( + (dispatch) => { + dispatch(submitMarkersAction()); + }, + 300000, + { + leading: true, + trailing: true, + }, +); + +export const submitMarkers = createAppAsyncThunk( + 'markers/submit', + (params: { immediate?: boolean }, { dispatch }) => { + debouncedSubmitMarkers(dispatch); + + if (params.immediate) { + debouncedSubmitMarkers.flush(); + } + }, +); + +export const fetchMarkers = createAppAsyncThunk( + 'markers/fetch', + async (_args, { getState }) => { + const response = + await api(getState).get>(`/api/v1/markers`); + + return { markers: response.data }; + }, +); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index f262fd8570..de597a3e3b 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -29,9 +29,14 @@ const setCSRFHeader = () => { void ready(setCSRFHeader); +export const authorizationTokenFromState = (getState?: GetState) => { + return ( + getState && (getState().meta.get('access_token', '') as string | false) + ); +}; + const authorizationHeaderFromState = (getState?: GetState) => { - const accessToken = - getState && (getState().meta.get('access_token', '') as string); + const accessToken = authorizationTokenFromState(getState); if (!accessToken) { return {}; diff --git a/app/javascript/mastodon/api_types/markers.ts b/app/javascript/mastodon/api_types/markers.ts new file mode 100644 index 0000000000..f7664fd7c1 --- /dev/null +++ b/app/javascript/mastodon/api_types/markers.ts @@ -0,0 +1,7 @@ +// See app/serializers/rest/account_serializer.rb + +export interface MarkerJSON { + last_read_id: string; + version: string; + updated_at: string; +} diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 3d42cc83f8..b88175e777 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -21,7 +21,7 @@ import history from './history'; import listAdder from './list_adder'; import listEditor from './list_editor'; import lists from './lists'; -import markers from './markers'; +import { markersReducer } from './markers'; import media_attachments from './media_attachments'; import meta from './meta'; import { modalReducer } from './modal'; @@ -77,7 +77,7 @@ const reducers = { suggestions, polls, trends, - markers, + markers: markersReducer, picture_in_picture, history, tags, diff --git a/app/javascript/mastodon/reducers/markers.js b/app/javascript/mastodon/reducers/markers.js deleted file mode 100644 index c7c5d99f61..0000000000 --- a/app/javascript/mastodon/reducers/markers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { - MARKERS_SUBMIT_SUCCESS, -} from '../actions/markers'; - - -const initialState = ImmutableMap({ - home: '0', - notifications: '0', -}); - -export default function markers(state = initialState, action) { - switch(action.type) { - case MARKERS_SUBMIT_SUCCESS: - if (action.home) { - state = state.set('home', action.home); - } - if (action.notifications) { - state = state.set('notifications', action.notifications); - } - return state; - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/markers.ts b/app/javascript/mastodon/reducers/markers.ts new file mode 100644 index 0000000000..ec85d0f173 --- /dev/null +++ b/app/javascript/mastodon/reducers/markers.ts @@ -0,0 +1,18 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { submitMarkersAction } from 'mastodon/actions/markers'; + +const initialState = { + home: '0', + notifications: '0', +}; + +export const markersReducer = createReducer(initialState, (builder) => { + builder.addCase( + submitMarkersAction.fulfilled, + (state, { payload: { home, notifications } }) => { + if (home) state.home = home; + if (notifications) state.notifications = notifications; + }, + ); +}); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index bc85936439..7230fabcae 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -13,7 +13,7 @@ import { unfocusApp, } from '../actions/app'; import { - MARKERS_FETCH_SUCCESS, + fetchMarkers, } from '../actions/markers'; import { notificationsUpdate, @@ -255,8 +255,8 @@ const recountUnread = (state, last_read_id) => { export default function notifications(state = initialState, action) { switch(action.type) { - case MARKERS_FETCH_SUCCESS: - return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state; + case fetchMarkers.fulfilled.type: + return action.payload.markers.notifications ? recountUnread(state, action.payload.markers.notifications.last_read_id) : state; case NOTIFICATIONS_MOUNT: return updateMounted(state); case NOTIFICATIONS_UNMOUNT: diff --git a/app/serializers/rest/marker_serializer.rb b/app/serializers/rest/marker_serializer.rb index 2eaf3d5076..b0c1c553ff 100644 --- a/app/serializers/rest/marker_serializer.rb +++ b/app/serializers/rest/marker_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class REST::MarkerSerializer < ActiveModel::Serializer + # Please update `app/javascript/mastodon/api_types/markers.ts` when making changes to the attributes + attributes :last_read_id, :version, :updated_at def last_read_id From b016f03637eb19ac22f9eb6dfc26ee07965d2456 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 27 Mar 2024 10:08:04 -0400 Subject: [PATCH 06/25] Pull out constant from `AccountWarning.recent` scope (#29767) --- app/models/account_warning.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb index a54387a562..7aa474887b 100644 --- a/app/models/account_warning.rb +++ b/app/models/account_warning.rb @@ -27,6 +27,8 @@ class AccountWarning < ApplicationRecord suspend: 4_000, }, suffix: :action + RECENT_PERIOD = 3.months.freeze + normalizes :text, with: ->(text) { text.to_s }, apply_to_nil: true belongs_to :account, inverse_of: :account_warnings @@ -37,7 +39,7 @@ class AccountWarning < ApplicationRecord scope :latest, -> { order(id: :desc) } scope :custom, -> { where.not(text: '') } - scope :recent, -> { where('account_warnings.created_at >= ?', 3.months.ago) } + scope :recent, -> { where(created_at: RECENT_PERIOD.ago..) } def statuses Status.with_discarded.where(id: status_ids || []) From 9fbe8d3a0cf33e1ec7f631b7651e9fb2bca8440e Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Wed, 27 Mar 2024 16:19:33 +0100 Subject: [PATCH 07/25] Rewrite PIP state in Typescript (#27645) Co-authored-by: Claire --- .../mastodon/actions/picture_in_picture.js | 46 ---------- .../mastodon/actions/picture_in_picture.ts | 31 +++++++ .../mastodon/containers/status_container.jsx | 2 +- .../picture_in_picture/components/footer.jsx | 2 +- .../picture_in_picture/components/header.jsx | 51 ----------- .../picture_in_picture/components/header.tsx | 46 ++++++++++ .../features/picture_in_picture/index.jsx | 89 ------------------- .../features/picture_in_picture/index.tsx | 79 ++++++++++++++++ app/javascript/mastodon/features/ui/index.jsx | 2 +- app/javascript/mastodon/reducers/index.ts | 4 +- .../mastodon/reducers/picture_in_picture.js | 26 ------ .../mastodon/reducers/picture_in_picture.ts | 56 ++++++++++++ app/javascript/mastodon/selectors/index.js | 2 +- 13 files changed, 218 insertions(+), 218 deletions(-) delete mode 100644 app/javascript/mastodon/actions/picture_in_picture.js create mode 100644 app/javascript/mastodon/actions/picture_in_picture.ts delete mode 100644 app/javascript/mastodon/features/picture_in_picture/components/header.jsx create mode 100644 app/javascript/mastodon/features/picture_in_picture/components/header.tsx delete mode 100644 app/javascript/mastodon/features/picture_in_picture/index.jsx create mode 100644 app/javascript/mastodon/features/picture_in_picture/index.tsx delete mode 100644 app/javascript/mastodon/reducers/picture_in_picture.js create mode 100644 app/javascript/mastodon/reducers/picture_in_picture.ts diff --git a/app/javascript/mastodon/actions/picture_in_picture.js b/app/javascript/mastodon/actions/picture_in_picture.js deleted file mode 100644 index 898375abeb..0000000000 --- a/app/javascript/mastodon/actions/picture_in_picture.js +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-check - -export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY'; -export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; - -/** - * @typedef MediaProps - * @property {string} src - * @property {boolean} muted - * @property {number} volume - * @property {number} currentTime - * @property {string} poster - * @property {string} backgroundColor - * @property {string} foregroundColor - * @property {string} accentColor - */ - -/** - * @param {string} statusId - * @param {string} accountId - * @param {string} playerType - * @param {MediaProps} props - * @returns {object} - */ -export const deployPictureInPicture = (statusId, accountId, playerType, props) => { - // @ts-expect-error - return (dispatch, getState) => { - // Do not open a player for a toot that does not exist - if (getState().hasIn(['statuses', statusId])) { - dispatch({ - type: PICTURE_IN_PICTURE_DEPLOY, - statusId, - accountId, - playerType, - props, - }); - } - }; -}; - -/* - * @return {object} - */ -export const removePictureInPicture = () => ({ - type: PICTURE_IN_PICTURE_REMOVE, -}); diff --git a/app/javascript/mastodon/actions/picture_in_picture.ts b/app/javascript/mastodon/actions/picture_in_picture.ts new file mode 100644 index 0000000000..d34b508a33 --- /dev/null +++ b/app/javascript/mastodon/actions/picture_in_picture.ts @@ -0,0 +1,31 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture'; +import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; + +interface DeployParams { + statusId: string; + accountId: string; + playerType: 'audio' | 'video'; + props: PIPMediaProps; +} + +export const removePictureInPicture = createAction('pip/remove'); + +export const deployPictureInPictureAction = + createAction('pip/deploy'); + +export const deployPictureInPicture = createAppAsyncThunk( + 'pip/deploy', + (args: DeployParams, { dispatch, getState }) => { + const { statusId } = args; + + // Do not open a player for a toot that does not exist + + // @ts-expect-error state.statuses is not yet typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + if (getState().hasIn(['statuses', statusId])) { + dispatch(deployPictureInPictureAction(args)); + } + }, +); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 7bd91c8c9d..da93a16b08 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -262,7 +262,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, deployPictureInPicture (status, type, mediaProps) { - dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); + dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps})); }, onInteractionModal (type, status) { diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index 8dfbf54cb6..a7d8356be7 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -210,4 +210,4 @@ class Footer extends ImmutablePureComponent { } -export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer))); +export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer))); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx deleted file mode 100644 index 31073d7387..0000000000 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { Avatar } from 'mastodon/components/avatar'; -import { DisplayName } from 'mastodon/components/display_name'; -import { IconButton } from 'mastodon/components/icon_button'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -const mapStateToProps = (state, { accountId }) => ({ - account: state.getIn(['accounts', accountId]), -}); - -class Header extends ImmutablePureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - statusId: PropTypes.string.isRequired, - account: ImmutablePropTypes.record.isRequired, - onClose: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { account, statusId, onClose, intl } = this.props; - - return ( -
- - - - - - -
- ); - } - -} - -export default connect(mapStateToProps)(injectIntl(Header)); diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.tsx b/app/javascript/mastodon/features/picture_in_picture/components/header.tsx new file mode 100644 index 0000000000..0f897dc441 --- /dev/null +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.tsx @@ -0,0 +1,46 @@ +import { defineMessages, useIntl } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { Avatar } from 'mastodon/components/avatar'; +import { DisplayName } from 'mastodon/components/display_name'; +import { IconButton } from 'mastodon/components/icon_button'; +import { useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}); + +interface Props { + accountId: string; + statusId: string; + onClose: () => void; +} + +export const Header: React.FC = ({ accountId, statusId, onClose }) => { + const account = useAppSelector((state) => state.accounts.get(accountId)); + + const intl = useIntl(); + + if (!account) return null; + + return ( +
+ + + + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/picture_in_picture/index.jsx b/app/javascript/mastodon/features/picture_in_picture/index.jsx deleted file mode 100644 index f087cd1b1d..0000000000 --- a/app/javascript/mastodon/features/picture_in_picture/index.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import { Component } from 'react'; - -import { connect } from 'react-redux'; - -import { removePictureInPicture } from 'mastodon/actions/picture_in_picture'; -import Audio from 'mastodon/features/audio'; -import Video from 'mastodon/features/video'; - -import Footer from './components/footer'; -import Header from './components/header'; - -const mapStateToProps = state => ({ - ...state.get('picture_in_picture'), -}); - -class PictureInPicture extends Component { - - static propTypes = { - statusId: PropTypes.string, - accountId: PropTypes.string, - type: PropTypes.string, - src: PropTypes.string, - muted: PropTypes.bool, - volume: PropTypes.number, - currentTime: PropTypes.number, - poster: PropTypes.string, - backgroundColor: PropTypes.string, - foregroundColor: PropTypes.string, - accentColor: PropTypes.string, - dispatch: PropTypes.func.isRequired, - }; - - handleClose = () => { - const { dispatch } = this.props; - dispatch(removePictureInPicture()); - }; - - render () { - const { type, src, currentTime, accountId, statusId } = this.props; - - if (!currentTime) { - return null; - } - - let player; - - if (type === 'video') { - player = ( -