mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fd9e6331 | ||
|
|
c14b7d4cb3 | ||
|
|
0f0ff5f3df |
No files matched your search
@@ -33,7 +33,6 @@ import {
|
||||
StartThreadFromMessageRequest,
|
||||
StartThreadRequest,
|
||||
ThreadChannelResponse,
|
||||
ThreadLocationQuery,
|
||||
ThreadMemberGetQuery,
|
||||
ThreadMemberListResponse,
|
||||
ThreadMembersListQuery,
|
||||
@@ -389,7 +388,6 @@ export function ThreadController(app: HonoApp) {
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_THREAD_MEMBER_PUT),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('query', ThreadLocationQuery),
|
||||
OpenAPI({
|
||||
operationId: 'join_thread',
|
||||
summary: 'Join a thread',
|
||||
@@ -415,7 +413,6 @@ export function ThreadController(app: HonoApp) {
|
||||
RateLimitMiddleware(RateLimitConfigs.CHANNEL_THREAD_MEMBER_DELETE),
|
||||
LoginRequired,
|
||||
Validator('param', ChannelIdParam),
|
||||
Validator('query', ThreadLocationQuery),
|
||||
OpenAPI({
|
||||
operationId: 'leave_thread',
|
||||
summary: 'Leave a thread',
|
||||
@@ -474,7 +471,6 @@ export function ThreadController(app: HonoApp) {
|
||||
LoginRequired,
|
||||
SelfThreadMemberAlias('join'),
|
||||
Validator('param', ChannelIdUserIdParam),
|
||||
Validator('query', ThreadLocationQuery),
|
||||
OpenAPI({
|
||||
operationId: 'add_thread_member',
|
||||
summary: 'Add a thread member',
|
||||
@@ -504,7 +500,6 @@ export function ThreadController(app: HonoApp) {
|
||||
LoginRequired,
|
||||
SelfThreadMemberAlias('leave'),
|
||||
Validator('param', ChannelIdUserIdParam),
|
||||
Validator('query', ThreadLocationQuery),
|
||||
OpenAPI({
|
||||
operationId: 'remove_thread_member',
|
||||
summary: 'Remove a thread member',
|
||||
|
||||
@@ -5850,13 +5850,6 @@
|
||||
"required": true,
|
||||
"schema": {"description": "The ID of the channel", "$ref": "#/components/schemas/SnowflakeType"},
|
||||
"description": "The ID of the channel"
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {"description": "Accepted and ignored", "type": "string", "maxLength": 100},
|
||||
"description": "Accepted and ignored"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -5915,13 +5908,6 @@
|
||||
"required": true,
|
||||
"schema": {"description": "The ID of the channel", "$ref": "#/components/schemas/SnowflakeType"},
|
||||
"description": "The ID of the channel"
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {"description": "Accepted and ignored", "type": "string", "maxLength": 100},
|
||||
"description": "Accepted and ignored"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6140,13 +6126,6 @@
|
||||
"required": true,
|
||||
"schema": {"description": "The ID of the user", "$ref": "#/components/schemas/SnowflakeType"},
|
||||
"description": "The ID of the user"
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {"description": "Accepted and ignored", "type": "string", "maxLength": 100},
|
||||
"description": "Accepted and ignored"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -6212,13 +6191,6 @@
|
||||
"required": true,
|
||||
"schema": {"description": "The ID of the user", "$ref": "#/components/schemas/SnowflakeType"},
|
||||
"description": "The ID of the user"
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {"description": "Accepted and ignored", "type": "string", "maxLength": 100},
|
||||
"description": "Accepted and ignored"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -31490,8 +31462,7 @@
|
||||
"maximum": 21600,
|
||||
"description": "Seconds a user has to wait before sending another message (0-21600)",
|
||||
"$ref": "#/components/schemas/Int32Type"
|
||||
},
|
||||
"location": {"description": "Accepted and ignored", "type": "string", "maxLength": 100}
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
@@ -36252,8 +36223,7 @@
|
||||
},
|
||||
"description": "The first message of the post",
|
||||
"$ref": "#/components/schemas/ForumThreadMessageRequest"
|
||||
},
|
||||
"location": {"description": "Accepted and ignored", "type": "string", "maxLength": 100}
|
||||
}
|
||||
},
|
||||
"required": ["name", "message"],
|
||||
"x-fluxer-experiment": "channel_threads"
|
||||
@@ -36272,8 +36242,7 @@
|
||||
"invitable": {
|
||||
"description": "Whether non-moderators can add other non-moderators (private threads)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"location": {"description": "Accepted and ignored", "type": "string", "maxLength": 100}
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"]
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from '@app/features/channel/components/MemberListItem.module.css';
|
||||
import {PreloadableUserPopout} from '@app/features/channel/components/PreloadableUserPopout';
|
||||
import Guilds from '@app/features/guild/state/Guilds';
|
||||
import {useMemberListCustomStatus} from '@app/features/member/hooks/useMemberListCustomStatus';
|
||||
import {useMemberListMobile} from '@app/features/member/hooks/useMemberListMobile';
|
||||
import {useMemberListPresence} from '@app/features/member/hooks/useMemberListPresence';
|
||||
import type {GuildMember} from '@app/features/member/models/GuildMember';
|
||||
import TypingIndicator from '@app/features/typing/state/TypingIndicator';
|
||||
@@ -51,6 +52,7 @@ interface MemberListItemProps {
|
||||
guildMember?: GuildMember;
|
||||
status?: StatusType;
|
||||
customStatus?: CustomStatus | null;
|
||||
isMobile?: boolean;
|
||||
isOwner?: boolean;
|
||||
roleColor?: string;
|
||||
displayName?: string;
|
||||
@@ -67,6 +69,7 @@ export const MemberListItem: React.FC<MemberListItemProps> = observer((props) =>
|
||||
guildMember,
|
||||
status: providedStatus,
|
||||
customStatus: providedCustomStatus,
|
||||
isMobile: providedIsMobile,
|
||||
isOwner = false,
|
||||
roleColor,
|
||||
displayName,
|
||||
@@ -91,6 +94,13 @@ export const MemberListItem: React.FC<MemberListItemProps> = observer((props) =>
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
const contextMenuTicketRef = useRef(0);
|
||||
const isCurrentUser = user.id === Authentication.currentUserId;
|
||||
const hookIsMobile = useMemberListMobile({
|
||||
guildId: guildId ?? '',
|
||||
channelId,
|
||||
userId: user.id,
|
||||
enabled: guildId !== undefined && providedIsMobile === undefined && !isCurrentUser,
|
||||
});
|
||||
const isMobile = isCurrentUser ? undefined : (providedIsMobile ?? hookIsMobile);
|
||||
const isTyping = TypingIndicator.isMemberListTyping(channelId, user.id, Authentication.currentUserId);
|
||||
const handleContextMenu = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
@@ -205,6 +215,7 @@ export const MemberListItem: React.FC<MemberListItemProps> = observer((props) =>
|
||||
showOffline={isCurrentUser || isTyping}
|
||||
guildId={guildId}
|
||||
status={status}
|
||||
isMobile={isMobile}
|
||||
avatarUrl={memberAvatarUrl}
|
||||
hoverAvatarUrl={memberHoverAvatarUrl}
|
||||
mediaSize={avatarMediaSize}
|
||||
|
||||
+5
@@ -11,6 +11,7 @@ import type {Channel} from '@app/features/channel/models/Channel';
|
||||
import type {Guild} from '@app/features/guild/models/Guild';
|
||||
import {OFFLINE_DESCRIPTOR, ONLINE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {resolveMemberListCustomStatus} from '@app/features/member/hooks/useMemberListCustomStatus';
|
||||
import {resolveMemberListMobile} from '@app/features/member/hooks/useMemberListMobile';
|
||||
import {resolveMemberListPresence} from '@app/features/member/hooks/useMemberListPresence';
|
||||
import {useMemberListSubscription} from '@app/features/member/hooks/useMemberListSubscription';
|
||||
import type {GuildMember} from '@app/features/member/models/GuildMember';
|
||||
@@ -118,6 +119,9 @@ const MobileMemberListItem = observer(
|
||||
const {i18n} = useLingui();
|
||||
const isTyping = TypingIndicator.isMemberListTyping(channelId, member.user.id, Authentication.currentUserId);
|
||||
const status = resolveMemberListPresence({guildId: guild.id, channelId, userId: member.user.id});
|
||||
const isMobile = member.isCurrentUser()
|
||||
? undefined
|
||||
: resolveMemberListMobile({guildId: guild.id, channelId, userId: member.user.id});
|
||||
const memberListCustomStatus = resolveMemberListCustomStatus({
|
||||
guildId: guild.id,
|
||||
channelId,
|
||||
@@ -175,6 +179,7 @@ const MobileMemberListItem = observer(
|
||||
showOffline={member.user.id === Authentication.currentUserId || isTyping}
|
||||
guildId={guild.id}
|
||||
status={status}
|
||||
isMobile={isMobile}
|
||||
avatarUrl={avatarUrl}
|
||||
hoverAvatarUrl={hoverAvatarUrl}
|
||||
mediaSize={MEDIA_PROXY_AVATAR_SIZE_DEFAULT}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import MemberSidebar from '@app/features/member/state/MemberSidebar';
|
||||
import {reaction} from 'mobx';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
interface UseMemberListMobileOptions {
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
userId: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function resolveMemberListMobile({
|
||||
guildId,
|
||||
channelId,
|
||||
userId,
|
||||
enabled = true,
|
||||
}: UseMemberListMobileOptions): boolean | undefined {
|
||||
if (!enabled) return undefined;
|
||||
return MemberSidebar.isMobile(guildId, channelId, userId) ?? undefined;
|
||||
}
|
||||
|
||||
export function useMemberListMobile({
|
||||
guildId,
|
||||
channelId,
|
||||
userId,
|
||||
enabled = true,
|
||||
}: UseMemberListMobileOptions): boolean | undefined {
|
||||
const [isMobile, setIsMobile] = useState(() => resolveMemberListMobile({guildId, channelId, userId, enabled}));
|
||||
useEffect(() => {
|
||||
return reaction(() => resolveMemberListMobile({guildId, channelId, userId, enabled}), setIsMobile, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}, [guildId, channelId, userId, enabled]);
|
||||
return isMobile;
|
||||
}
|
||||
@@ -61,6 +61,25 @@ export class GuildMember {
|
||||
this.mentionFlags = (guildMember.mention_flags ?? 0) as MentionReplyPreference;
|
||||
}
|
||||
|
||||
matches(guildMember: GuildMemberData): boolean {
|
||||
const until = guildMember.communication_disabled_until
|
||||
? new Date(guildMember.communication_disabled_until).getTime()
|
||||
: null;
|
||||
return (
|
||||
this.nick === (guildMember.nick ?? null) &&
|
||||
this.avatar === (guildMember.avatar ?? null) &&
|
||||
this.banner === (guildMember.banner ?? null) &&
|
||||
this.accentColor === (guildMember.accent_color ?? null) &&
|
||||
this.mute === (guildMember.mute ?? false) &&
|
||||
this.deaf === (guildMember.deaf ?? false) &&
|
||||
this.profileFlags === (guildMember.profile_flags ?? 0) &&
|
||||
this.mentionFlags === (guildMember.mention_flags ?? 0) &&
|
||||
(this.communicationDisabledUntil?.getTime() ?? null) === until &&
|
||||
this.roles.size === guildMember.roles.length &&
|
||||
guildMember.roles.every((roleId) => this.roles.has(roleId))
|
||||
);
|
||||
}
|
||||
|
||||
isAvatarUnset(): boolean {
|
||||
return (this.profileFlags & GuildMemberProfileFlags.AVATAR_UNSET) !== 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import SelectedGuild from '@app/features/navigation/state/SelectedGuild';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
import type {GuildMemberData} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import type {User as WireUser} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {makeAutoObservable} from 'mobx';
|
||||
|
||||
type Members = Record<string, GuildMember>;
|
||||
@@ -215,6 +216,19 @@ class GuildMembers {
|
||||
this.nonMembers[guildId]?.delete(member.user.id);
|
||||
}
|
||||
|
||||
hydrateOrRefresh(guildId: string, member: GuildMemberData): void {
|
||||
const existing = this.members[guildId]?.[member.user.id];
|
||||
if (!existing) {
|
||||
this.hydrateIfMissing(guildId, member);
|
||||
return;
|
||||
}
|
||||
Users.handleUserUpdate(member.user as WireUser);
|
||||
if (existing.user === Users.getUser(member.user.id) && existing.matches(member)) {
|
||||
return;
|
||||
}
|
||||
this.members[guildId][member.user.id] = new GuildMember(guildId, member);
|
||||
}
|
||||
|
||||
handleMemberRemove(guildId: string, userId: string): void {
|
||||
this.markAsNonMember(guildId, userId);
|
||||
const existingMembers = this.members[guildId];
|
||||
|
||||
@@ -58,6 +58,7 @@ interface MemberListState {
|
||||
requestedRanges: NormalizedMemberListRanges;
|
||||
subscribedRanges: NormalizedMemberListRanges;
|
||||
presences: Map<string, StatusType>;
|
||||
mobileUserIds: Set<string>;
|
||||
customStatuses: Map<string, CustomStatus | null>;
|
||||
knownCustomStatuses: Map<string, CustomStatus | null>;
|
||||
}
|
||||
@@ -69,6 +70,7 @@ interface MemberListRow {
|
||||
member?: GuildMemberData;
|
||||
presence?: {
|
||||
status?: string;
|
||||
mobile?: boolean;
|
||||
custom_status?: GatewayCustomStatusPayload | null;
|
||||
} | null;
|
||||
}
|
||||
@@ -76,6 +78,7 @@ interface MemberListRow {
|
||||
type MemberListOperationMember = GuildMemberData & {
|
||||
presence?: {
|
||||
status?: string;
|
||||
mobile?: boolean;
|
||||
custom_status?: GatewayCustomStatusPayload | null;
|
||||
} | null;
|
||||
};
|
||||
@@ -655,6 +658,7 @@ class MemberSidebar {
|
||||
const newRows = new Map<number, MemberListRow>();
|
||||
const newItems = new Map<number, MemberListItem>();
|
||||
const newPresences = new Map<string, StatusType>();
|
||||
const newMobileUserIds = new Set<string>();
|
||||
const newCustomStatuses = new Map<string, CustomStatus | null>();
|
||||
const newMembersByUserId = new Map<string, GuildMemberData>();
|
||||
const nextKnownCustomStatuses = new Map(listState.knownCustomStatuses);
|
||||
@@ -704,6 +708,9 @@ class MemberSidebar {
|
||||
const presenceStatus = this.extractPresenceFromRow(row);
|
||||
if (presenceStatus) {
|
||||
newPresences.set(userId, presenceStatus);
|
||||
if (presenceStatus !== StatusTypes.OFFLINE && row.presence?.mobile === true) {
|
||||
newMobileUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
if (row.presence && Object.hasOwn(row.presence, 'custom_status')) {
|
||||
const customStatus = fromGatewayCustomStatus(row.presence.custom_status ?? null);
|
||||
@@ -757,6 +764,7 @@ class MemberSidebar {
|
||||
listState.items = newItems;
|
||||
listState.membersByUserId = newMembersByUserId;
|
||||
listState.presences = newPresences;
|
||||
listState.mobileUserIds = newMobileUserIds;
|
||||
listState.customStatuses = newCustomStatuses;
|
||||
listState.knownCustomStatuses = nextKnownCustomStatuses;
|
||||
listState.subscribedRanges = getHydratedMemberListRangesFromNormalized(
|
||||
@@ -1183,6 +1191,14 @@ class MemberSidebar {
|
||||
return listState.presences.get(userId) ?? null;
|
||||
}
|
||||
|
||||
isMobile(guildId: string, listId: string, userId: string): boolean | null {
|
||||
const listState = this.getList(guildId, listId);
|
||||
if (!listState?.presences.has(userId)) {
|
||||
return null;
|
||||
}
|
||||
return listState.mobileUserIds.has(userId);
|
||||
}
|
||||
|
||||
getCustomStatus(guildId: string, listId: string, userId: string): CustomStatus | null | undefined {
|
||||
const listState = this.getList(guildId, listId);
|
||||
if (!listState) {
|
||||
@@ -1262,6 +1278,7 @@ class MemberSidebar {
|
||||
items: new Map(),
|
||||
membersByUserId: new Map(),
|
||||
presences: new Map(),
|
||||
mobileUserIds: new Set(),
|
||||
customStatuses: new Map(),
|
||||
};
|
||||
}
|
||||
@@ -1274,10 +1291,14 @@ class MemberSidebar {
|
||||
prunedMembersByUserId.set(item.data.userId, item.data.member);
|
||||
}
|
||||
const prunedPresences = new Map<string, StatusType>();
|
||||
const prunedMobileUserIds = new Set<string>();
|
||||
const prunedCustomStatuses = new Map<string, CustomStatus | null>();
|
||||
for (const [userId, status] of listState.presences) {
|
||||
if (retainedUserIds.has(userId)) {
|
||||
prunedPresences.set(userId, status);
|
||||
if (listState.mobileUserIds.has(userId)) {
|
||||
prunedMobileUserIds.add(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [userId, customStatus] of listState.customStatuses) {
|
||||
@@ -1302,6 +1323,7 @@ class MemberSidebar {
|
||||
items: prunedItems,
|
||||
membersByUserId: prunedMembersByUserId,
|
||||
presences: prunedPresences,
|
||||
mobileUserIds: prunedMobileUserIds,
|
||||
customStatuses: prunedCustomStatuses,
|
||||
};
|
||||
}
|
||||
@@ -1350,6 +1372,7 @@ class MemberSidebar {
|
||||
requestedRanges: normalizeMemberListRanges(requestedRanges),
|
||||
subscribedRanges: EMPTY_MEMBER_LIST_RANGES,
|
||||
presences: new Map(),
|
||||
mobileUserIds: new Set(),
|
||||
customStatuses: new Map(),
|
||||
knownCustomStatuses: new Map(),
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ export function useThreadRosterSubscription(thread: Channel | undefined): void {
|
||||
interface RosterRow {
|
||||
user: User;
|
||||
status: StatusType;
|
||||
mobile?: boolean;
|
||||
customStatus: CustomStatus | null;
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ const RosterGroup = observer(({id, label, members, thread, ownerId}: RosterGroup
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.membersList} data-flx="threads.thread-members-panel.roster-group.members-list">
|
||||
{members.map(({user, status, customStatus}) => (
|
||||
{members.map(({user, status, mobile, customStatus}) => (
|
||||
<MemberListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
@@ -74,6 +75,7 @@ const RosterGroup = observer(({id, label, members, thread, ownerId}: RosterGroup
|
||||
guildId={thread.guildId}
|
||||
guildMember={thread.guildId ? (GuildMembers.getMember(thread.guildId, user.id) ?? undefined) : undefined}
|
||||
status={status}
|
||||
isMobile={mobile}
|
||||
customStatus={customStatus}
|
||||
isOwner={user.id === ownerId}
|
||||
disableBackdrop={true}
|
||||
@@ -95,7 +97,7 @@ function resolveMembers(guildId: string | undefined, roster: ReadonlyArray<Threa
|
||||
const status = LocalPresence.getStatus();
|
||||
members.push({user, status, customStatus: isOfflineStatus(status) ? null : LocalPresence.customStatus});
|
||||
} else {
|
||||
members.push({user, status: entry.status, customStatus: entry.customStatus});
|
||||
members.push({user, status: entry.status, mobile: entry.mobile, customStatus: entry.customStatus});
|
||||
}
|
||||
}
|
||||
members.sort((a, b) =>
|
||||
|
||||
@@ -53,7 +53,7 @@ vi.mock('@app/features/ui/commands/ContextMenuCommands', () => ({close: vi.fn()}
|
||||
vi.mock('@app/features/ui/commands/PopoutCommands', () => ({closeAll: vi.fn()}));
|
||||
vi.mock('@app/features/guild/state/Guilds', () => ({default: {getGuild: () => ({ownerId: 'owner', mfaLevel: 0})}}));
|
||||
vi.mock('@app/features/member/state/GuildMembers', () => ({
|
||||
default: {getMember: () => null, hydrateIfMissing: () => {}},
|
||||
default: {getMember: () => null, hydrateOrRefresh: () => {}},
|
||||
}));
|
||||
vi.mock('@app/features/permissions/state/Permission', () => ({
|
||||
default: {
|
||||
@@ -616,7 +616,7 @@ describe('thread roster', () => {
|
||||
const release = ThreadRoster.subscribe(GUILD, THREAD_A);
|
||||
ThreadRoster.handleListUpdate(
|
||||
listUpdate([
|
||||
{user_id: ONLINE, presence: {status: 'online', afk: false, mobile: false, custom_status: null}},
|
||||
{user_id: ONLINE, presence: {status: 'online', afk: false, mobile: true, custom_status: null}},
|
||||
{
|
||||
user_id: IDLE,
|
||||
presence: {
|
||||
@@ -630,14 +630,15 @@ describe('thread roster', () => {
|
||||
]),
|
||||
);
|
||||
expect(ThreadRoster.getMembers(THREAD_A)).toEqual([
|
||||
{userId: ONLINE, joinTimestamp: null, status: 'online', customStatus: null},
|
||||
{userId: ONLINE, joinTimestamp: null, status: 'online', mobile: true, customStatus: null},
|
||||
{
|
||||
userId: IDLE,
|
||||
joinTimestamp: null,
|
||||
status: 'idle',
|
||||
mobile: false,
|
||||
customStatus: expect.objectContaining({text: 'im spooky goop', emojiName: 'nahua_smug'}),
|
||||
},
|
||||
{userId: OFFLINE, joinTimestamp: null, status: 'offline', customStatus: null},
|
||||
{userId: OFFLINE, joinTimestamp: null, status: 'offline', mobile: false, customStatus: null},
|
||||
]);
|
||||
release();
|
||||
});
|
||||
@@ -652,7 +653,7 @@ describe('thread roster', () => {
|
||||
);
|
||||
ThreadRoster.handleListUpdate(listUpdate([{user_id: ONLINE, presence: {status: 'dnd'}}]));
|
||||
expect(ThreadRoster.getMembers(THREAD_A)).toEqual([
|
||||
{userId: ONLINE, joinTimestamp: null, status: 'dnd', customStatus: null},
|
||||
{userId: ONLINE, joinTimestamp: null, status: 'dnd', mobile: false, customStatus: null},
|
||||
]);
|
||||
release();
|
||||
ThreadRoster.handleListUpdate(listUpdate([{user_id: ONLINE, presence: {status: 'online'}}]));
|
||||
|
||||
@@ -20,7 +20,11 @@ export interface ThreadMemberListPayload {
|
||||
join_timestamp: string | null;
|
||||
flags: number;
|
||||
member: GuildMemberData | null;
|
||||
presence?: {status?: string | null; custom_status?: GatewayCustomStatusPayload | null} | null;
|
||||
presence?: {
|
||||
status?: string | null;
|
||||
mobile?: boolean;
|
||||
custom_status?: GatewayCustomStatusPayload | null;
|
||||
} | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -28,6 +32,7 @@ export interface ThreadRosterMember {
|
||||
readonly userId: string;
|
||||
readonly joinTimestamp: string | null;
|
||||
readonly status: StatusType;
|
||||
readonly mobile: boolean;
|
||||
readonly customStatus: CustomStatus | null;
|
||||
}
|
||||
|
||||
@@ -78,13 +83,14 @@ class ThreadRoster {
|
||||
const members: Array<ThreadRosterMember> = [];
|
||||
for (const entry of payload.members) {
|
||||
if (entry.member) {
|
||||
GuildMembers.hydrateIfMissing(payload.guild_id, entry.member);
|
||||
GuildMembers.hydrateOrRefresh(payload.guild_id, entry.member);
|
||||
}
|
||||
const status = normalizeStatus(entry.presence?.status);
|
||||
members.push({
|
||||
userId: entry.user_id,
|
||||
joinTimestamp: entry.join_timestamp,
|
||||
status,
|
||||
mobile: !isOfflineStatus(status) && entry.presence?.mobile === true,
|
||||
customStatus: isOfflineStatus(status) ? null : fromGatewayCustomStatus(entry.presence?.custom_status),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface StatusAwareAvatarProps {
|
||||
guildId?: string | null;
|
||||
mediaSize?: MediaProxyImageSize;
|
||||
status?: string | null;
|
||||
isMobile?: boolean;
|
||||
animateStatusCutout?: boolean;
|
||||
}
|
||||
|
||||
@@ -54,6 +55,7 @@ export const StatusAwareAvatar: React.FC<StatusAwareAvatarProps> = observer(
|
||||
guildId,
|
||||
mediaSize,
|
||||
status: externalStatus,
|
||||
isMobile: externalIsMobile,
|
||||
animateStatusCutout,
|
||||
}) => {
|
||||
if (!user) {
|
||||
@@ -66,7 +68,7 @@ export const StatusAwareAvatar: React.FC<StatusAwareAvatarProps> = observer(
|
||||
} else if (externalStatus == null) {
|
||||
status = getStatusWithTransientFallback(user.id);
|
||||
}
|
||||
const isMobile = shouldDisablePresence ? false : Presence.isMobile(user.id);
|
||||
const isMobile = shouldDisablePresence ? false : (externalIsMobile ?? Presence.isMobile(user.id));
|
||||
return (
|
||||
<Avatar
|
||||
user={user}
|
||||
|
||||
@@ -84,12 +84,6 @@ Joining a locked thread needs an enrolled authenticator while the guild [MFA lev
|
||||
| --- | --- | --- |
|
||||
| channel_id | snowflake | The ID of the thread |
|
||||
|
||||
### Query parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
@@ -131,12 +125,6 @@ Adding to a locked thread needs an enrolled authenticator while the guild [MFA l
|
||||
| channel_id | snowflake | The ID of the thread |
|
||||
| user_id | snowflake | The ID of the user to add |
|
||||
|
||||
### Query parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
@@ -176,12 +164,6 @@ A timed-out member can leave a thread. A member who can no longer view the threa
|
||||
| --- | --- | --- |
|
||||
| channel_id | snowflake | The ID of the thread |
|
||||
|
||||
### Query parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
@@ -219,12 +201,6 @@ Removing a member needs an enrolled authenticator while the guild [MFA level](/h
|
||||
| channel_id | snowflake | The ID of the thread |
|
||||
| user_id | snowflake | The ID of the user to remove |
|
||||
|
||||
### Query parameters
|
||||
|
||||
| Field | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
### Response
|
||||
|
||||
| Status | Body | Condition |
|
||||
|
||||
@@ -184,7 +184,6 @@ Starts a public thread from a message in a guild text channel, or an announcemen
|
||||
| name | string | The name of the thread (1-100 characters) |
|
||||
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) |
|
||||
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
`rate_limit_per_user` defaults to the `default_thread_rate_limit_per_user` of the parent, or `0` when the parent has none.
|
||||
|
||||
@@ -252,7 +251,6 @@ The parent type selects the body. Either body can be sent as multipart form data
|
||||
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) |
|
||||
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
|
||||
| invitable?<sup>1</sup> | boolean | Whether a member who is not a thread moderator can add members who are not thread moderators (default true) |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
<sup>1</sup> Applies only to a private thread
|
||||
|
||||
@@ -266,7 +264,6 @@ The parent type selects the body. Either body can be sent as multipart form data
|
||||
| auto_archive_duration? | integer | The [auto archive duration](#auto-archive-durations) in minutes (default 4320) |
|
||||
| rate_limit_per_user? | integer | The slowmode interval in seconds (0-21,600) |
|
||||
| type?<sup>2</sup> | integer | The thread type (`11` only) |
|
||||
| location? | string | A value Fluxer accepts and ignores (max 100 characters) |
|
||||
|
||||
<sup>1</sup> Required when the channel has `REQUIRE_TAG`. A tag marked `moderated` needs a thread moderator
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ An over-length string draws two entries for the one path.
|
||||
| double_tap_reaction? | [reaction emoji](#reaction-emoji-object) object | Emoji a double tap on a message adds as a reaction |
|
||||
| announcement_prompts? | [announcement prompts state](#announcement-prompts-state-object) object | Hidden announcement channel prompt state |
|
||||
| double_tap_action | int32 | [Double tap action](#double-tap-actions) on a message |
|
||||
| swipe_right_to_left_action | int32 | [Swipe right to left action](#swipe-right-to-left-actions) in a channel |
|
||||
|
||||
<sup>1</sup> The entries live inside the snapshot, and the account [memes](/http-api/memes/) collection holds none of them
|
||||
|
||||
@@ -886,3 +887,13 @@ The `double_tap_action` field selects what a double tap on a message does in the
|
||||
| 1 | DOUBLE_TAP_ACTION_REACT | Add the `double_tap_reaction` emoji as a reaction |
|
||||
| 2 | DOUBLE_TAP_ACTION_EDIT | Edit the message, when it is the account's own message |
|
||||
| 3 | DOUBLE_TAP_ACTION_NONE | Do nothing |
|
||||
|
||||
## Swipe right to left actions
|
||||
|
||||
The `swipe_right_to_left_action` field selects what a right to left swipe in a channel does in the mobile client.
|
||||
|
||||
| Value | Name | Description |
|
||||
| --- | --- | --- |
|
||||
| 0 | SWIPE_ACTION_UNSPECIFIED | No explicit action is selected |
|
||||
| 1 | SWIPE_ACTION_CHANNEL_DETAILS | Open the channel details |
|
||||
| 2 | SWIPE_ACTION_REPLY | Reply to the swiped message |
|
||||
@@ -299,6 +299,13 @@ after_store_event(guild_member_remove, Data, Tab, State) ->
|
||||
_ -> []
|
||||
end,
|
||||
State;
|
||||
after_store_event(guild_member_update, Data, _Tab, State) ->
|
||||
case guild_state_member:extract_user_id(Data) of
|
||||
UserId when is_integer(UserId) ->
|
||||
guild_thread_subscriptions:member_changed(UserId, State);
|
||||
_ ->
|
||||
State
|
||||
end;
|
||||
after_store_event(_Event, _Data, _Tab, State) ->
|
||||
State.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
member_lists_changed/2,
|
||||
resync_after_load/1,
|
||||
presence_changed/2,
|
||||
member_changed/2,
|
||||
handle_list_flush/1,
|
||||
refresh_presence_users/1,
|
||||
export_handoff/1,
|
||||
@@ -306,6 +307,10 @@ presence_changed(UserId, #{thread_presence_users := Users} = State) ->
|
||||
presence_changed(_UserId, State) ->
|
||||
State.
|
||||
|
||||
-spec member_changed(integer(), guild_state()) -> guild_state().
|
||||
member_changed(UserId, State) ->
|
||||
presence_changed(UserId, State).
|
||||
|
||||
-spec mark_dirty([integer()], guild_state()) -> guild_state().
|
||||
mark_dirty([], State) ->
|
||||
State;
|
||||
|
||||
@@ -1486,6 +1486,35 @@ thread_member_list_follows_presence_changes_test() ->
|
||||
?assertEqual(State, guild_thread_subscriptions:presence_changed(?A, State))
|
||||
end).
|
||||
|
||||
thread_member_list_follows_member_updates_test() ->
|
||||
with_guild(fun(State) ->
|
||||
Subscribed = guild_thread_subscriptions:update(
|
||||
<<"a">>, #{member_lists => [?T1]}, State
|
||||
),
|
||||
[_] = received(a, thread_member_list_update, drain()),
|
||||
Update = fun(UserId, Nick) ->
|
||||
#{
|
||||
<<"user">> => #{<<"id">> => integer_to_binary(UserId)},
|
||||
<<"roles">> => [],
|
||||
<<"nick">> => Nick
|
||||
}
|
||||
end,
|
||||
Changed = dispatch(guild_member_update, Update(?A, <<"renamed">>), Subscribed),
|
||||
?assertEqual([], received(a, thread_member_list_update, drain())),
|
||||
_ = guild_thread_subscriptions:handle_list_flush(Changed),
|
||||
[#{<<"members">> := Members}] = received(a, thread_member_list_update, drain()),
|
||||
?assertEqual(
|
||||
[<<"renamed">>],
|
||||
[
|
||||
maps:get(<<"nick">>, Member)
|
||||
|| #{<<"user_id">> := Id, <<"member">> := Member} <- Members,
|
||||
Id =:= integer_to_binary(?A)
|
||||
]
|
||||
),
|
||||
Unlisted = dispatch(guild_member_update, Update(?C, <<"other">>), Subscribed),
|
||||
?assertNot(maps:is_key(thread_list_dirty, Unlisted))
|
||||
end).
|
||||
|
||||
handoff_through_an_old_node_round_trips_test() ->
|
||||
with_guild(fun(State) ->
|
||||
Exported = guild_handoff:export_handoff_state(State),
|
||||
|
||||
@@ -53,6 +53,7 @@ message SyncedPreferences {
|
||||
ChannelFrecencyState channel_frecency = 115;
|
||||
AnnouncementPromptsState announcement_prompts = 116;
|
||||
DoubleTapAction double_tap_action = 117;
|
||||
SwipeAction swipe_right_to_left_action = 118;
|
||||
|
||||
}
|
||||
|
||||
@@ -288,6 +289,12 @@ enum DoubleTapAction {
|
||||
DOUBLE_TAP_ACTION_NONE = 3;
|
||||
}
|
||||
|
||||
enum SwipeAction {
|
||||
SWIPE_ACTION_UNSPECIFIED = 0;
|
||||
SWIPE_ACTION_CHANNEL_DETAILS = 1;
|
||||
SWIPE_ACTION_REPLY = 2;
|
||||
}
|
||||
|
||||
message ReactionEmoji {
|
||||
optional string id = 1;
|
||||
string name = 2;
|
||||
|
||||
@@ -38,7 +38,6 @@ import {z} from 'zod';
|
||||
|
||||
const EXPERIMENT = {experiment: 'channel_threads'} as const;
|
||||
const EMOJI_NAME_MAX_LENGTH = 64;
|
||||
const LOCATION_MAX_LENGTH = 100;
|
||||
|
||||
export const ForumTagRequest = withSchemaMetadata(
|
||||
z.object({
|
||||
@@ -141,7 +140,6 @@ export const StartForumThreadRequest = withSchemaMetadata(
|
||||
.describe('Seconds a user has to wait before sending another message (0-21600)'),
|
||||
applied_tags: AppliedTagsType.optional(),
|
||||
message: ForumThreadMessageRequest.describe('The first message of the post'),
|
||||
location: z.string().max(LOCATION_MAX_LENGTH).optional().describe('Accepted and ignored'),
|
||||
}),
|
||||
EXPERIMENT,
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import {z} from 'zod';
|
||||
|
||||
const THREAD_MEMBERS_LIST_MAX_LIMIT = 100;
|
||||
const LOCATION_MAX_LENGTH = 100;
|
||||
|
||||
export const ThreadAutoArchiveDurationSchema = createInt32EnumType(
|
||||
[
|
||||
@@ -43,13 +42,10 @@ const ThreadRateLimitPerUserType = Int32Type.max(THREAD_RATE_LIMIT_PER_USER_MAX)
|
||||
'Seconds a user has to wait before sending another message (0-21600)',
|
||||
);
|
||||
|
||||
const LocationType = z.string().max(LOCATION_MAX_LENGTH).optional().describe('Accepted and ignored');
|
||||
|
||||
export const StartThreadFromMessageRequest = z.object({
|
||||
name: ThreadNameType,
|
||||
auto_archive_duration: ThreadAutoArchiveDurationSchema.optional(),
|
||||
rate_limit_per_user: ThreadRateLimitPerUserType.optional(),
|
||||
location: LocationType,
|
||||
});
|
||||
|
||||
export type StartThreadFromMessageRequest = z.infer<typeof StartThreadFromMessageRequest>;
|
||||
@@ -68,15 +64,10 @@ export const StartThreadRequest = z.object({
|
||||
auto_archive_duration: ThreadAutoArchiveDurationSchema.optional(),
|
||||
rate_limit_per_user: ThreadRateLimitPerUserType.optional(),
|
||||
invitable: z.boolean().optional().describe('Whether non-moderators can add other non-moderators (private threads)'),
|
||||
location: LocationType,
|
||||
});
|
||||
|
||||
export type StartThreadRequest = z.infer<typeof StartThreadRequest>;
|
||||
|
||||
export const ThreadLocationQuery = z.object({
|
||||
location: LocationType,
|
||||
});
|
||||
|
||||
export const ThreadMembersListQuery = z.object({
|
||||
with_member: QueryBooleanType.describe('Whether to include a guild member object for each thread member'),
|
||||
after: SnowflakeType.optional().describe('Get thread members after this user ID'),
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ChatInputSettingsSchema,
|
||||
DoubleTapAction,
|
||||
ReactionEmojiSchema,
|
||||
SwipeAction,
|
||||
} from '@fluxer/schema/src/gen/fluxer/user/preferences/v1/preferences_pb';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
@@ -110,6 +111,13 @@ describe('SyncedPreferencesCodec', () => {
|
||||
expect(decoded.doubleTapAction).toBe(action);
|
||||
}
|
||||
});
|
||||
it('round-trips every swipe right to left action', () => {
|
||||
for (const action of [SwipeAction.CHANNEL_DETAILS, SwipeAction.REPLY]) {
|
||||
const original = create(SyncedPreferencesSchema, {swipeRightToLeftAction: action});
|
||||
const decoded = decodeSyncedPreferences(encodeSyncedPreferences(original));
|
||||
expect(decoded.swipeRightToLeftAction).toBe(action);
|
||||
}
|
||||
});
|
||||
it('produces canonical encodings for equal inputs', () => {
|
||||
const a = create(SyncedPreferencesSchema, {
|
||||
accessibility: create(AccessibilitySettingsSchema, {fontSize: 14}),
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
|
||||
* Describes the file fluxer/user/preferences/v1/preferences.proto.
|
||||
*/
|
||||
export const file_fluxer_user_preferences_v1_preferences: GenFile = /*@__PURE__*/
|
||||
fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEiwRYKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUSUgoUYW5ub3VuY2VtZW50X3Byb21wdHMYdCABKAsyNC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Bbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGUSRgoRZG91YmxlX3RhcF9hY3Rpb24YdSABKA4yKy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Eb3VibGVUYXBBY3Rpb25CIAoeX3NhdmVfY2FtZXJhX3VwbG9hZHNfdG9fZGV2aWNlIrABChJTcGVsbGNoZWNrU2V0dGluZ3MSFAoHZW5hYmxlZBgBIAEoCEgAiAEBEhEKCWxhbmd1YWdlcxgCIAMoCRIbChNwZXJzb25hbF9kaWN0aW9uYXJ5GAMgAygJEhgKC2F1dG9fZGV0ZWN0GAQgASgISAGIAQESEwoGZW5naW5lGAUgASgJSAKIAQFCCgoIX2VuYWJsZWRCDgoMX2F1dG9fZGV0ZWN0QgkKB19lbmdpbmUi5gEKFFNlYXJjaEVuZ2luZVNldHRpbmdzEiIKFXRleHRfc2VhcmNoX2VuZ2luZV9pZBgBIAEoCUgAiAEBEisKHnJldmVyc2VfaW1hZ2Vfc2VhcmNoX2VuZ2luZV9pZBgCIAEoCUgBiAEBEiQKF3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkGAMgASgJSAKIAQFCGAoWX3RleHRfc2VhcmNoX2VuZ2luZV9pZEIhCh9fcmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkQhoKGF90cmFuc2xhdGlvbl9wcm92aWRlcl9pZCK1AQoSUHJpdmFjeVByZWZlcmVuY2VzEh8KF2Rpc2FibGVfc3RyZWFtX3ByZXZpZXdzGAEgASgIEhwKD3Nob3dfYWN0aXZlX25vdxgCIAEoCEgAiAEBEioKHXByZXVwbG9hZF9tZXNzYWdlX2F0dGFjaG1lbnRzGAMgASgISAGIAQFCEgoQX3Nob3dfYWN0aXZlX25vd0IgCh5fcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMiUAoWTG9jYWxVc2VyU3BhbU92ZXJyaWRlcxIYChBzcGFtbWVyX3VzZXJfaWRzGAEgAygJEhwKFG5vdF9zcGFtbWVyX3VzZXJfaWRzGAIgAygJIisKFlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSEQoJd3JhcF90ZXh0GAEgASgIItwBChJTaWRlYmFyUHJlZmVyZW5jZXMSHAoUaW5saW5lX2Rtc19jb2xsYXBzZWQYASABKAgSLAofc2hLine truncated
|
||||
fileDesc("CixmbHV4ZXIvdXNlci9wcmVmZXJlbmNlcy92MS9wcmVmZXJlbmNlcy5wcm90bxIaZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEijhcKEVN5bmNlZFByZWZlcmVuY2VzEkgKDWFjY2Vzc2liaWxpdHkYASABKAsyMS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5U2V0dGluZ3MSUwoXYWNjZXNzaWJpbGl0eV9vdmVycmlkZXMYAiABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5BY2Nlc3NpYmlsaXR5T3ZlcnJpZGVzEksKD3RleHR1YWxfcHJldmlldxgDIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlRleHR1YWxQcmV2aWV3U2V0dGluZ3MSQgoMZW1vamlfcGlja2VyGBQgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlQaWNrZXJTdGF0ZRJGCg5zdGlja2VyX3BpY2tlchgVIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN0aWNrZXJQaWNrZXJTdGF0ZRJCCgxtZW1lc19waWNrZXIYFiABKAsyLC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW1lc1BpY2tlclN0YXRlEjUKBWVtb2ppGBcgASgLMiYuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuRW1vamlTdGF0ZRJUChRlbW9qaV9zdGlja2VyX2xheW91dBgYIAEoCzI2LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkVtb2ppU3RpY2tlckxheW91dFNldHRpbmdzEkYKDWZhdm9yaXRlX2dpZnMYGSABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5GYXZvcml0ZUdpZlNldHRpbmdzEj0KCWZhdm9yaXRlcxgoIAEoCzIqLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkZhdm9yaXRlc1N0YXRlEksKD3JlY2VudF9tZW50aW9ucxgpIAEoCzIyLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlJlY2VudE1lbnRpb25zU2V0dGluZ3MSPwoHc2lkZWJhchgqIAEoCzIuLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlNpZGViYXJQcmVmZXJlbmNlcxJACgttZW1iZXJfbGlzdBgrIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLk1lbWJlckxpc3RTdGF0ZRJICg91bnJlYWRfY2hhbm5lbHMYLCABKAsyLy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5VbnJlYWRDaGFubmVsc1N0YXRlEkoKEG1lbnRpb25fZnJlY2VuY3kYLSABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5NZW50aW9uRnJlY2VuY3lTdGF0ZRI9CgduYWdiYXJzGDwgASgLMiwuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuTmFnYmFyRGlzbWlzc2FscxJHChFkaXNtaXNzZWRfdXBzZWxscxg9IAEoCzIsLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLkRpc21pc3NlZFVwc2VsbHMSTgoVZ3VpbGRfbnNmd19hZ3JlZW1lbnRzGD4gASgLMi8uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGROc2Z3QWdyZWVtZW50cxI8Cgl3aGF0c19uZXcYPyABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5XaGF0c05ld1N0YXRlEj8KB3ByaXZhY3kYUCABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Qcml2YWN5UHJlZmVyZW5jZXMSUAoUbG9jYWxfc3BhbV9vdmVycmlkZXMYUSABKAsyMi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Mb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhUKDXNhbml0aXplX3VybHMYUiABKAgSOAoFc291bmQYZCABKAsyKS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Tb3VuZFNldHRpbmdzEkIKCnNwZWxsY2hlY2sYZSABKAsyLi5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TcGVsbGNoZWNrU2V0dGluZ3MSSAoOc2VhcmNoX2VuZ2luZXMYZiABKAsyMC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5TZWFyY2hFbmdpbmVTZXR0aW5ncxJPChFwZXJtaXNzaW9uX2xheW91dBhnIAEoCzI0LmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlBlcm1pc3Npb25MYXlvdXRTZXR0aW5ncxJSChNndWlsZF9tZW1iZXJfbGF5b3V0GGggASgLMjUuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRNZW1iZXJMYXlvdXRTZXR0aW5ncxJLCg1ndWlsZF9mb2xkZXJzGGkgASgLMjQuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuR3VpbGRGb2xkZXJFeHBhbmRlZFN0YXRlElAKFGhpZGRlbl9ndWlsZF9idXR0b25zGGogASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSGlkZGVuR3VpbGRMaXN0QnV0dG9ucxJPChNrZXlib2FyZF9tb2RlX2ludHJvGGsgASgLMjIuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuS2V5Ym9hcmRNb2RlSW50cm9TdGF0ZRJRChBpbnB1dF9tb25pdG9yaW5nGGwgASgLMjcuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuSW5wdXRNb25pdG9yaW5nUHJvbXB0c1N0YXRlEkQKDXZvaWNlX3Byb21wdHMYbSABKAsyLS5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Wb2ljZVByb21wdHNTdGF0ZRJACgtzdWRvX3Byb21wdBhuIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLlN1ZG9Qcm9tcHRTdGF0ZRI9CghrZXliaW5kcxhvIAEoCzIrLmZsdXhlci51c2VyLnByZWZlcmVuY2VzLnYxLktleWJpbmRTZXR0aW5ncxJBCgpjaGF0X2lucHV0GHAgASgLMi0uZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhdElucHV0U2V0dGluZ3MSKgodc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UYcSABKAhIAIgBARJGChNkb3VibGVfdGFwX3JlYWN0aW9uGHIgASgLMikuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuUmVhY3Rpb25FbW9qaRJKChBjaGFubmVsX2ZyZWNlbmN5GHMgASgLMjAuZmx1eGVyLnVzZXIucHJlZmVyZW5jZXMudjEuQ2hhbm5lbEZyZWNlbmN5U3RhdGUSUgoUYW5ub3VuY2VtZW50X3Byb21wdHMYdCABKAsyNC5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Bbm5vdW5jZW1lbnRQcm9tcHRzU3RhdGUSRgoRZG91YmxlX3RhcF9hY3Rpb24YdSABKA4yKy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Eb3VibGVUYXBBY3Rpb24SSwoac3dpcGVfcmlnaHRfdG9fbGVmdF9hY3Rpb24YdiABKA4yJy5mbHV4ZXIudXNlci5wcmVmZXJlbmNlcy52MS5Td2lwZUFjdGlvbkIgCh5fc2F2ZV9jYW1lcmFfdXBsb2Fkc190b19kZXZpY2UisAEKElNwZWxsY2hlY2tTZXR0aW5ncxIUCgdlbmFibGVkGAEgASgISACIAQESEQoJbGFuZ3VhZ2VzGAIgAygJEhsKE3BlcnNvbmFsX2RpY3Rpb25hcnkYAyADKAkSGAoLYXV0b19kZXRlY3QYBCABKAhIAYgBARITCgZlbmdpbmUYBSABKAlIAogBAUIKCghfZW5hYmxlZEIOCgxfYXV0b19kZXRlY3RCCQoHX2VuZ2luZSLmAQoUU2VhcmNoRW5naW5lU2V0dGluZ3MSIgoVdGV4dF9zZWFyY2hfZW5naW5lX2lkGAEgASgJSACIAQESKwoecmV2ZXJzZV9pbWFnZV9zZWFyY2hfZW5naW5lX2lkGAIgASgJSAGIAQESJAoXdHJhbnNsYXRpb25fcHJvdmlkZXJfaWQYAyABKAlIAogBAUIYChZfdGV4dF9zZWFyY2hfZW5naW5lX2lkQiEKH19yZXZlcnNlX2ltYWdlX3NlYXJjaF9lbmdpbmVfaWRCGgoYX3RyYW5zbGF0aW9uX3Byb3ZpZGVyX2lkIrUBChJQcml2YWN5UHJlZmVyZW5jZXMSHwoXZGlzYWJsZV9zdHJlYW1fcHJldmlld3MYASABKAgSHAoPc2hvd19hY3RpdmVfbm93GAIgASgISACIAQESKgodcHJldXBsb2FkX21lc3NhZ2VfYXR0YWNobWVudHMYAyABKAhIAYgBAUISChBfc2hvd19hY3RpdmVfbm93QiAKHl9wcmV1cGxvYWRfbWVzc2FnZV9hdHRhY2htZW50cyJQChZMb2NhbFVzZXJTcGFtT3ZlcnJpZGVzEhgKEHNwYW1tZXJfdXNlcl9pZHMYASADKAkSHAoUbm90X3NwYW1tZXJfdXNlcl9pZHMYAiADKAkiKwoWVGV4dHVhbFByZXZpZXdTZXR0aW5ncxILine truncated
|
||||
|
||||
/**
|
||||
* @generated from message fluxer.user.preferences.v1.SyncedPreferences
|
||||
@@ -215,6 +215,11 @@ export type SyncedPreferences = Message<"fluxer.user.preferences.v1.SyncedPrefer
|
||||
* @generated from field: fluxer.user.preferences.v1.DoubleTapAction double_tap_action = 117;
|
||||
*/
|
||||
doubleTapAction: DoubleTapAction;
|
||||
|
||||
/**
|
||||
* @generated from field: fluxer.user.preferences.v1.SwipeAction swipe_right_to_left_action = 118;
|
||||
*/
|
||||
swipeRightToLeftAction: SwipeAction;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1288,3 +1293,29 @@ export enum DoubleTapAction {
|
||||
*/
|
||||
export const DoubleTapActionSchema: GenEnum<DoubleTapAction> = /*@__PURE__*/
|
||||
enumDesc(file_fluxer_user_preferences_v1_preferences, 4);
|
||||
|
||||
/**
|
||||
* @generated from enum fluxer.user.preferences.v1.SwipeAction
|
||||
*/
|
||||
export enum SwipeAction {
|
||||
/**
|
||||
* @generated from enum value: SWIPE_ACTION_UNSPECIFIED = 0;
|
||||
*/
|
||||
UNSPECIFIED = 0,
|
||||
|
||||
/**
|
||||
* @generated from enum value: SWIPE_ACTION_CHANNEL_DETAILS = 1;
|
||||
*/
|
||||
CHANNEL_DETAILS = 1,
|
||||
|
||||
/**
|
||||
* @generated from enum value: SWIPE_ACTION_REPLY = 2;
|
||||
*/
|
||||
REPLY = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the enum fluxer.user.preferences.v1.SwipeAction.
|
||||
*/
|
||||
export const SwipeActionSchema: GenEnum<SwipeAction> = /*@__PURE__*/
|
||||
enumDesc(file_fluxer_user_preferences_v1_preferences, 5);
|
||||
Reference in new issue
Block a user