diff --git a/app/javascript/flavours/glitch/api_types/accounts.ts b/app/javascript/flavours/glitch/api_types/accounts.ts index 5bf3e64288c76e..fdbd7523fc15cd 100644 --- a/app/javascript/flavours/glitch/api_types/accounts.ts +++ b/app/javascript/flavours/glitch/api_types/accounts.ts @@ -13,7 +13,7 @@ export interface ApiAccountRoleJSON { } // See app/serializers/rest/account_serializer.rb -export interface ApiAccountJSON { +export interface BaseApiAccountJSON { acct: string; avatar: string; avatar_static: string; @@ -45,3 +45,12 @@ export interface ApiAccountJSON { memorial?: boolean; hide_collections: boolean; } + +// See app/serializers/rest/muted_account_serializer.rb +export interface ApiMutedAccountJSON extends BaseApiAccountJSON { + mute_expires_at?: string | null; +} + +// For now, we have the same type representing both `Account` and `MutedAccount` +// objects, but we should refactor this in the future. +export type ApiAccountJSON = ApiMutedAccountJSON; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx index 941b1254f7cd02..4f45189e59cefa 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_with_status.tsx @@ -16,6 +16,7 @@ import { import type { IconProp } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon'; import Status from 'flavours/glitch/containers/status_container'; +import { getStatusHidden } from 'flavours/glitch/selectors/filters'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { DisplayedName } from './displayed_name'; @@ -51,6 +52,12 @@ export const NotificationWithStatus: React.FC<{ (state) => state.statuses.getIn([statusId, 'visibility']) === 'direct', ); + const isFiltered = useAppSelector( + (state) => + statusId && + getStatusHidden(state, { id: statusId, contextType: 'notifications' }), + ); + const handlers = useMemo( () => ({ open: () => { @@ -77,7 +84,7 @@ export const NotificationWithStatus: React.FC<{ [dispatch, statusId], ); - if (!statusId) return null; + if (!statusId || isFiltered) return null; return ( diff --git a/app/javascript/flavours/glitch/models/account.ts b/app/javascript/flavours/glitch/models/account.ts index 0b698ead3d1930..799265831ea4c5 100644 --- a/app/javascript/flavours/glitch/models/account.ts +++ b/app/javascript/flavours/glitch/models/account.ts @@ -95,6 +95,9 @@ export const accountDefaultValues: AccountShape = { limited: false, moved: null, hide_collections: false, + // This comes from `ApiMutedAccountJSON`, but we should eventually + // store that in a different object. + mute_expires_at: null, }; const AccountFactory = ImmutableRecord(accountDefaultValues); diff --git a/app/javascript/flavours/glitch/reducers/notification_groups.ts b/app/javascript/flavours/glitch/reducers/notification_groups.ts index 3eece4b1d09a0d..d1dacc7964e134 100644 --- a/app/javascript/flavours/glitch/reducers/notification_groups.ts +++ b/app/javascript/flavours/glitch/reducers/notification_groups.ts @@ -559,7 +559,10 @@ export const notificationGroupsReducer = createReducer( compareId(state.lastReadId, mostRecentGroup.page_max_id) < 0 ) state.lastReadId = mostRecentGroup.page_max_id; - commitLastReadId(state); + + // We don't call `commitLastReadId`, because that is conditional + // and we want to unconditionally update the state instead. + state.readMarkerId = state.lastReadId; }) .addCase(fetchMarkers.fulfilled, (state, action) => { if ( diff --git a/app/javascript/flavours/glitch/selectors/filters.ts b/app/javascript/flavours/glitch/selectors/filters.ts new file mode 100644 index 00000000000000..5d7bfcbdc7a075 --- /dev/null +++ b/app/javascript/flavours/glitch/selectors/filters.ts @@ -0,0 +1,50 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import type { RootState } from 'flavours/glitch/store'; +import { toServerSideType } from 'flavours/glitch/utils/filters'; + +// TODO: move to `app/javascript/flavours/glitch/models` and use more globally +type Filter = Immutable.Map; + +// TODO: move to `app/javascript/flavours/glitch/models` and use more globally +type FilterResult = Immutable.Map; + +export const getFilters = createSelector( + [ + (state: RootState) => state.filters as Immutable.Map, + (_, { contextType }: { contextType: string }) => contextType, + ], + (filters, contextType) => { + if (!contextType) { + return null; + } + + const now = new Date(); + const serverSideType = toServerSideType(contextType); + + return filters.filter((filter) => { + const context = filter.get('context') as Immutable.List; + const expiration = filter.get('expires_at') as Date | null; + return ( + context.includes(serverSideType) && + (expiration === null || expiration > now) + ); + }); + }, +); + +export const getStatusHidden = ( + state: RootState, + { id, contextType }: { id: string; contextType: string }, +) => { + const filters = getFilters(state, { contextType }); + if (filters === null) return false; + + const filtered = state.statuses.getIn([id, 'filtered']) as + | Immutable.List + | undefined; + return filtered?.some( + (result) => + filters.getIn([result.get('filter'), 'filter_action']) === 'hide', + ); +}; diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index 1b2d63ae36d1b5..7c9a68cba41429 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -1,22 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; -import { toServerSideType } from 'flavours/glitch/utils/filters'; - import { me } from '../initial_state'; -export { makeGetAccount } from "./accounts"; - -const getFilters = createSelector([state => state.get('filters'), (_, { contextType }) => contextType], (filters, contextType) => { - if (!contextType) { - return null; - } +import { getFilters } from './filters'; - const now = new Date(); - const serverSideType = toServerSideType(contextType); - - return filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now)); -}); +export { makeGetAccount } from "./accounts"; export const makeGetStatus = () => { return createSelector( diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 9bc3c7758ea741..fadc678de3c25f 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1050,6 +1050,12 @@ a.name-tag, color: var(--user-role-accent); } +.applications-list { + .icon { + vertical-align: middle; + } +} + .announcements-list, .filters-list { border: 1px solid var(--background-border-color); diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 261aaae4950fb6..7c2f19bde17284 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -8525,18 +8525,50 @@ noscript { background: rgba($base-overlay-background, 0.5); } +.list-adder, .list-editor { - background: $ui-base-color; + backdrop-filter: var(--background-filter); + background: var(--modal-background-color); + border: 1px solid var(--modal-border-color); flex-direction: column; border-radius: 8px; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); width: 380px; overflow: hidden; @media screen and (width <= 420px) { width: 90%; } +} + +.list-adder { + &__lists { + height: 50vh; + border-radius: 0 0 8px 8px; + overflow-y: auto; + } + + .list { + padding: 10px; + border-bottom: 1px solid var(--background-border-color); + } + .list__wrapper { + display: flex; + } + + .list__display-name { + flex: 1 1 auto; + overflow: hidden; + text-decoration: none; + font-size: 16px; + padding: 10px; + display: flex; + align-items: center; + gap: 4px; + } +} + +.list-editor { h4 { padding: 15px 0; background: lighten($ui-base-color, 13%); @@ -8548,12 +8580,10 @@ noscript { .drawer__pager { height: 50vh; - border-radius: 4px; + border: 0; } .drawer__inner { - border-radius: 0 0 8px 8px; - &.backdrop { width: calc(100% - 60px); box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); @@ -8562,6 +8592,7 @@ noscript { } &__accounts { + background: unset; overflow-y: auto; } @@ -8580,47 +8611,6 @@ noscript { } } -.list-adder { - background: $ui-base-color; - flex-direction: column; - border-radius: 8px; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); - width: 380px; - overflow: hidden; - - @media screen and (width <= 420px) { - width: 90%; - } - - &__account { - background: lighten($ui-base-color, 13%); - } - - &__lists { - background: lighten($ui-base-color, 13%); - height: 50vh; - border-radius: 0 0 8px 8px; - overflow-y: auto; - } - - .list { - padding: 10px; - border-bottom: 1px solid var(--background-border-color); - } - - .list__wrapper { - display: flex; - } - - .list__display-name { - flex: 1 1 auto; - overflow: hidden; - text-decoration: none; - font-size: 16px; - padding: 10px; - } -} - .focal-point { position: relative; cursor: move;