From f8b0431a212258b823f3cc8e1b59c18b503eba89 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:08:51 -0500 Subject: [PATCH 01/34] fix: Shrink home-screen flex layout to single-column for mobile Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/src/interface/Home.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/client/src/interface/Home.tsx b/packages/client/src/interface/Home.tsx index 46be1482e2..617d6b9c42 100644 --- a/packages/client/src/interface/Home.tsx +++ b/packages/client/src/interface/Home.tsx @@ -66,6 +66,8 @@ const Buttons = styled("div", { gap: "8px", padding: "8px", display: "flex", + flexWrap: "wrap", + justifyContent: "center", borderRadius: "var(--borderRadius-lg)", color: "var(--md-sys-color-on-surface-variant)", From 93ecdca902da0c786eda6694405d79bba9c5ab4f Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:01:17 -0500 Subject: [PATCH 02/34] fix: More mobile UI fixes - Settings menu margin shrink and use "Back" button in sidebar instead of "Esc" button on right when screen width < 800px - Message box doesn't grow from placeholder, overflow with ... instead - Hide GIF button when typing Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../app/interface/settings/Settings.tsx | 12 ++++++++---- .../app/interface/settings/UserSettings.tsx | 5 +++-- .../app/interface/settings/_layout/Content.tsx | 8 ++++++++ .../app/interface/settings/_layout/Sidebar.tsx | 14 +++++++++----- .../settings/_layout/SidebarButton.tsx | 5 +++++ .../app/interface/settings/index.tsx | 6 +++++- .../interface/settings/user/_AccountCard.tsx | 18 ++++++++++++++++++ .../messaging/composition/MessageBox.tsx | 11 +++++++++++ .../features/texteditor/TextEditor2.tsx | 3 +++ .../interface/channels/text/Composition.tsx | 13 +++++++------ 10 files changed, 77 insertions(+), 18 deletions(-) diff --git a/packages/client/components/app/interface/settings/Settings.tsx b/packages/client/components/app/interface/settings/Settings.tsx index bd60cadc86..43e71a98a9 100644 --- a/packages/client/components/app/interface/settings/Settings.tsx +++ b/packages/client/components/app/interface/settings/Settings.tsx @@ -89,7 +89,11 @@ export function Settings(props: SettingsProps & SettingsConfiguration) { navigate, }} > - + {(list) => ( <> @@ -156,14 +160,14 @@ export function Settings(props: SettingsProps & SettingsConfiguration) { */ function MemoisedList(props: { context: never; - list: (context: never) => SettingsList; + onClose?: () => void; + list: (context: never, onClose?: () => void) => SettingsList; children: (list: Accessor>) => JSX.Element; }) { /** * Generate list of categories / links */ - const list = createMemo(() => props.list(props.context)); - + const list = createMemo(() => props.list(props.context, props.onClose)); return <>{props.children(list)}; } diff --git a/packages/client/components/app/interface/settings/UserSettings.tsx b/packages/client/components/app/interface/settings/UserSettings.tsx index 4f5c0f05b8..01a503f598 100644 --- a/packages/client/components/app/interface/settings/UserSettings.tsx +++ b/packages/client/components/app/interface/settings/UserSettings.tsx @@ -30,7 +30,7 @@ import MdWorkspacePremium from "@material-design-icons/svg/outlined/workspace_pr import pkg from "../../../../../../package.json"; import { SettingsConfiguration } from "."; -import { AccountCard } from "./user/_AccountCard"; +import { AccountCard, BackCard } from "./user/_AccountCard"; import { MyAccount } from "./user/Account"; import AdvancedSettings from "./user/Advanced"; import { AppearanceMenu } from "./user/appearance"; @@ -111,7 +111,7 @@ const Config: SettingsConfiguration<{ server: Server }> = { * Generate list of categories / entries for client settings * @returns List */ - list() { + list(_, onClose) { const { pop, openModal } = useModals(); const { logout } = useClientLifecycle(); @@ -119,6 +119,7 @@ const Config: SettingsConfiguration<{ server: Server }> = { context: null!, prepend: ( +
diff --git a/packages/client/components/app/interface/settings/_layout/Content.tsx b/packages/client/components/app/interface/settings/_layout/Content.tsx index 75d0fc1dea..adf17a331d 100644 --- a/packages/client/components/app/interface/settings/_layout/Content.tsx +++ b/packages/client/components/app/interface/settings/_layout/Content.tsx @@ -88,6 +88,10 @@ const InnerContent = styled("div", { padding: "80px 32px", justifyContent: "stretch", zIndex: 1, + + "@media (max-width: 800px)": { + padding: "12px" + } }, }); @@ -126,5 +130,9 @@ const CloseAction = styled("div", { color: "var(--md-sys-color-on-surface)", fontSize: "0.75rem", }, + + "@media (max-width: 800px)": { + display: "none" + } }, }); diff --git a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx index c395090231..3aa3f08795 100644 --- a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx +++ b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx @@ -20,18 +20,18 @@ import { */ export function SettingsSidebar(props: { list: Accessor>; - setPage: Setter; page: Accessor; }) { const { navigate } = useSettingsNavigation(); + const list = props.list(); /** * Select first page on load */ onMount(() => { if (!props.page()) { - props.setPage(props.list().entries[0].entries[0].id); + props.setPage(list.entries[0].entries[0].id); } }); @@ -40,8 +40,8 @@ export function SettingsSidebar(props: {
- {props.list().prepend} - + {list.prepend} + {(category) => ( @@ -87,7 +87,7 @@ export function SettingsSidebar(props: { )} - {props.list().append} + {list.append}
@@ -123,6 +123,10 @@ const Content = styled("div", { "& a > div": { margin: 0, }, + + "@media (max-width: 800px)": { + padding: "12px 0" + } }, }); diff --git a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx index a24c271494..acb2528de5 100644 --- a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx +++ b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx @@ -33,6 +33,11 @@ export const SidebarButton = styled("a", { background: "var(--md-sys-color-primary-container)", }, }, + mobileOnly: { + true: { + "@media (min-width: 800px)": {display: "none"} + } + } }, }); diff --git a/packages/client/components/app/interface/settings/index.tsx b/packages/client/components/app/interface/settings/index.tsx index 78f574a637..82be8caf6c 100644 --- a/packages/client/components/app/interface/settings/index.tsx +++ b/packages/client/components/app/interface/settings/index.tsx @@ -9,9 +9,13 @@ export { Settings } from "./Settings"; export type SettingsConfiguration = { /** * Generate list of categories and entries + * @param props State information * @returns List */ - list: (context: T) => SettingsList; + list: ( + context: T, + onClose?: () => void + ) => SettingsList; /** * Render the title of the current breadcrumb key diff --git a/packages/client/components/app/interface/settings/user/_AccountCard.tsx b/packages/client/components/app/interface/settings/user/_AccountCard.tsx index 209ef601bb..f0ffdf0a81 100644 --- a/packages/client/components/app/interface/settings/user/_AccountCard.tsx +++ b/packages/client/components/app/interface/settings/user/_AccountCard.tsx @@ -3,6 +3,8 @@ import { Trans } from "@lingui-solid/solid/macro"; import { useClient } from "@revolt/client"; import { Avatar, OverflowingText, Ripple, typography } from "@revolt/ui"; +import MdArrowBack from "@material-design-icons/svg/outlined/arrow_back.svg?component-solid"; + import { useSettingsNavigation } from "../Settings"; import { SidebarButton, @@ -40,3 +42,19 @@ export function AccountCard() { ); } + +export function BackCard(props: { + onClose?: () => void; +}) { + return ( + + + + + + Back + + + + ); +} \ No newline at end of file diff --git a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx index 8702e13e9c..3a517e3fd3 100644 --- a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx @@ -95,6 +95,7 @@ interface Props { const Base = styled("div", { base: { flexGrow: 1, + minWidth: 0, paddingInlineEnd: "var(--gap-md)", paddingBlock: "var(--gap-sm)", @@ -103,6 +104,16 @@ const Base = styled("div", { display: "flex", background: "var(--md-sys-color-surface-container-high)", color: "var(--md-sys-color-on-surface)", + + "& .cm-content": { + minWidth: 0 + }, + "& .cm-placeholder": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%" + } }, variants: { hasActionsAppend: { diff --git a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx index 3377fd5266..f1545f2aaf 100644 --- a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx +++ b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx @@ -76,6 +76,9 @@ export function TextEditor2(props: Props) { const codeMirror = document.createElement("div"); codeMirror.className = editor; + //Custom CSS + codeMirror.style.minWidth = '0'; + /** * Handle 'Enter' key presses * Submit only if not currently in a code block diff --git a/packages/client/src/interface/channels/text/Composition.tsx b/packages/client/src/interface/channels/text/Composition.tsx index 7106d31f5b..0bbd088acf 100644 --- a/packages/client/src/interface/channels/text/Composition.tsx +++ b/packages/client/src/interface/channels/text/Composition.tsx @@ -475,17 +475,18 @@ export function MessageComposition(props: Props) { > {(triggerProps) => ( <> - - - gif - - + + + + gif + + + emoticon -
)} From a7de4d3d1d2ee285ec2603aaa9bf3caea5aea321 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Feb 2026 22:31:26 -0500 Subject: [PATCH 03/34] fix: Mobile UI marches onwards - Disable hover action buttons for DMs, server list, and messages on mobile because these are too easy to accidentially tap - Move the text editor fixes from MessageBox.tsx to codeMirrorLineWrap.ts to make it act as a CodeMirror extension - Allow newlines with enter key on mobile instead of sending (Attempted send with Ctrl + Enter on mobile, but this doesn't work for some reason. Eg. this would matter for mobile external keyboard or Android Desktop mode.) Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/state/index.tsx | 3 ++ .../messaging/composition/MessageBox.tsx | 10 ------ .../features/messaging/elements/Container.tsx | 9 +++++- .../features/texteditor/TextEditor2.tsx | 7 +++-- .../features/texteditor/codeMirrorLineWrap.ts | 19 +++++++++--- .../navigation/channels/HomeSidebar.tsx | 31 +++++++++++-------- .../navigation/channels/ServerSidebar.tsx | 9 ++---- 7 files changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx index 1ed491a6bb..f061815f98 100644 --- a/packages/client/components/state/index.tsx +++ b/packages/client/components/state/index.tsx @@ -11,6 +11,7 @@ import { SetStoreFunction, createStore } from "solid-js/store"; import equal from "fast-deep-equal"; import localforage from "localforage"; +import { isMobileBrowser } from "@livekit/components-core"; import { AbstractStore, Store } from "./stores"; import { Auth } from "./stores/Auth"; import { Draft } from "./stores/Draft"; @@ -50,6 +51,7 @@ export class State { private store: Store; private setStore: SetStoreFunction; private writeQueue: Record; + isMobile: boolean; // define all stores auth = new Auth(this); @@ -105,6 +107,7 @@ export class State { this.store = store as never; this.setStore = setStore; this.writeQueue = {}; + this.isMobile = isMobileBrowser(); } /** diff --git a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx index 3a517e3fd3..ca8ffd58c6 100644 --- a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx @@ -104,16 +104,6 @@ const Base = styled("div", { display: "flex", background: "var(--md-sys-color-surface-container-high)", color: "var(--md-sys-color-on-surface)", - - "& .cm-content": { - minWidth: 0 - }, - "& .cm-placeholder": { - overflow: "hidden", - textOverflow: "ellipsis", - whiteSpace: "nowrap", - width: "100%" - } }, variants: { hasActionsAppend: { diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index 46f1764d42..e209963d76 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -13,6 +13,7 @@ import { Time, } from "@revolt/ui/components/utils"; +import { useState } from "@revolt/state"; import { MessageToolbar } from "./MessageToolbar"; interface CommonProps { @@ -306,6 +307,7 @@ const CompactInfo = styled(Row, { */ export function MessageContainer(props: Props) { const { t } = useLingui(); + const { isMobile } = useState(); return (
diff --git a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx index f1545f2aaf..c955d7db59 100644 --- a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx +++ b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx @@ -9,6 +9,7 @@ import { css } from "styled-system/css"; import { scrollableStyles } from "../../../directives/scrollable"; import { AutoCompleteSearchSpace } from "../../utils/autoComplete"; +import { useState } from "@revolt/state"; import { codeMirrorAutoComplete } from "./codeMirrorAutoComplete"; import { isInFencedCodeBlock } from "./codeMirrorCommon"; import { smartLineWrapping } from "./codeMirrorLineWrap"; @@ -73,11 +74,12 @@ const placeholderCompartment = new Compartment(); export function TextEditor2(props: Props) { const editorScrollbarClasses = scrollableStyles(); + const { isMobile } = useState(); const codeMirror = document.createElement("div"); codeMirror.className = editor; //Custom CSS - codeMirror.style.minWidth = '0'; + codeMirror.style.minWidth = "0"; /** * Handle 'Enter' key presses @@ -85,7 +87,8 @@ export function TextEditor2(props: Props) { */ const enterKeymap = keymap.of([ { - key: "Enter", + key: isMobile ? "Ctrl-Enter" : "Enter", + //TODO: This is not working right, mobile seems to not trigger the function run: (view) => { if (!props.onComplete) return false; diff --git a/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts b/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts index 428e376fcb..d8e9fdae4f 100644 --- a/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts +++ b/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts @@ -155,18 +155,27 @@ const lineWrapStyles = EditorView.theme({ ".cm-line.linewrap-indent": { // The tiny offset appears to make the indent more reliable, // for unknown reasons. - "text-indent": "calc(-1 * var(--indented) - 0.1px)", - "padding-left": "calc(var(--indented) + var(--cm-left-padding, 4px))", + textIndent: "calc(-1 * var(--indented) - 0.1px)", + paddingLeft: "calc(var(--indented) + var(--cm-left-padding, 4px))", }, ".linewrap-whitespace": { - "font-family": "monospace, monospace", + fontFamily: "monospace, monospace", // Prevent slightly-oversided monospace fonts from changing line heights // when indented, but also changes the height of lines with only whitespace... - // "font-size": "0.9em", + // fontSize: ".9em", }, ".cm-line > *": { - "text-indent": "0", + textIndent: 0, }, + ".cm-content": { + minWidth: 0 + }, + ".cm-placeholder": { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + width: "100%" + } }); export const smartLineWrapping = [ diff --git a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx index de8579e531..fbfb8dbb0f 100644 --- a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx @@ -25,6 +25,7 @@ import { Symbol } from "@revolt/ui/components/utils/Symbol"; import MdClose from "@material-design-icons/svg/outlined/close.svg?component-solid"; +import { useState } from "@revolt/state"; import { SidebarBase } from "./common"; interface Props { @@ -55,6 +56,7 @@ export const HomeSidebar = (props: Props) => { const navigate = useNavigate(); const location = useLocation(); const { openModal } = useModals(); + const { isMobile } = useState(); const savedNotesChannelId = createMemo(() => props.openSavedNotes()); @@ -186,6 +188,7 @@ export const HomeSidebar = (props: Props) => { style={item.style} channel={item.item} active={item.item.id === props.channelId} + isMobile={isMobile} />
)} @@ -261,12 +264,12 @@ const NameStatusStack = styled("div", { * Single conversation entry */ function Entry( - props: { channel: Channel; active: boolean } /*& Omit< + props: { channel: Channel; active: boolean; isMobile: boolean } /*& Omit< ComponentProps, "href" >*/, ) { - const [local, remote] = splitProps(props, ["channel", "active"]); + const [local, remote] = splitProps(props, ["channel", "active", "isMobile"]); const { t } = useLingui(); const { openModal } = useModals(); @@ -331,17 +334,19 @@ function Entry( } actions={ - { - e.preventDefault(); - openModal({ - type: "delete_channel", - channel: local.channel, - }); - }} - > - - + + { + e.preventDefault(); + openModal({ + type: "delete_channel", + channel: local.channel, + }); + }} + > + + + } use:floating={{ contextMenu: () => diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index 2b0b857eed..507cc8ecc7 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -112,9 +112,7 @@ export const ServerSidebar = (props: Props) => { // TODO: we want it to feel smooth when navigating through channels, so we'll want to select channels immediately but not actually navigate until we're done moving through them /** Navigates to the channel offset from the current one, wrapping around if needed */ const _navigateChannel = (byOffset: number) => { - if (props.channelId == null) { - return; - } + if (props.channelId == null) return; const channels = visibleChannels(); @@ -510,7 +508,7 @@ function Entry( } actions={ - <> + - - + } > From e8e74cacf615431831f6afc2c5679fff0e3a0911 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Feb 2026 23:22:17 -0500 Subject: [PATCH 04/34] fix: Add back button to Channel & Server settings Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../components/app/interface/settings/ChannelSettings.tsx | 4 +++- .../components/app/interface/settings/ServerSettings.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/client/components/app/interface/settings/ChannelSettings.tsx b/packages/client/components/app/interface/settings/ChannelSettings.tsx index d00fa69372..9b39b37e7a 100644 --- a/packages/client/components/app/interface/settings/ChannelSettings.tsx +++ b/packages/client/components/app/interface/settings/ChannelSettings.tsx @@ -19,6 +19,7 @@ import { ChannelPermissionsEditor } from "./channel/permissions/ChannelPermissio import { ChannelPermissionsOverview } from "./channel/permissions/ChannelPermissionsOverview"; import { ViewWebhook } from "./channel/webhooks/ViewWebhook"; import { WebhooksList } from "./channel/webhooks/WebhooksList"; +import { BackCard } from "./user/_AccountCard"; const Config: SettingsConfiguration = { /** @@ -98,11 +99,12 @@ const Config: SettingsConfiguration = { * Generate list of categories / entries for channel settings * @returns List */ - list(channel) { + list(channel, onClose) { const { openModal } = useModals(); return { context: channel, + prepend: , entries: [ { title: , diff --git a/packages/client/components/app/interface/settings/ServerSettings.tsx b/packages/client/components/app/interface/settings/ServerSettings.tsx index c7b48002ed..28d8803327 100644 --- a/packages/client/components/app/interface/settings/ServerSettings.tsx +++ b/packages/client/components/app/interface/settings/ServerSettings.tsx @@ -24,6 +24,7 @@ import { EmojiList } from "./server/emojis/EmojiList"; import { ListServerInvites } from "./server/invites/ListServerInvites"; import { ServerRoleEditor } from "./server/roles/ServerRoleEditor"; import { ServerRoleOverview } from "./server/roles/ServerRoleOverview"; +import { BackCard } from "./user/_AccountCard"; const Config: SettingsConfiguration = { /** @@ -89,12 +90,13 @@ const Config: SettingsConfiguration = { * Generate list of categories / entries for server settings * @returns List */ - list(server) { + list(server, onClose) { const user = useUser(); const { openModal } = useModals(); return { context: server, + prepend: , entries: [ { title: , From e3fd1a8ebe138d5b3a58f41f40427ff3fb40d071 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:59:32 -0500 Subject: [PATCH 05/34] fix: Simplified CSS - Found a better spot to put the media query so that it's all in one place for easier maintence - Hide breadcrums at top in "My Account" panel to fix visual bug where there's still a gap there despite no title - Center ESC button text properly Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../interface/settings/_layout/Content.tsx | 34 ++++++++----------- .../interface/settings/_layout/Sidebar.tsx | 6 +--- .../settings/_layout/SidebarButton.tsx | 7 ---- .../interface/settings/user/_AccountCard.tsx | 8 ++--- .../components/modal/modals/Settings.tsx | 31 ++++++++++++----- 5 files changed, 40 insertions(+), 46 deletions(-) diff --git a/packages/client/components/app/interface/settings/_layout/Content.tsx b/packages/client/components/app/interface/settings/_layout/Content.tsx index adf17a331d..efe91ef8c7 100644 --- a/packages/client/components/app/interface/settings/_layout/Content.tsx +++ b/packages/client/components/app/interface/settings/_layout/Content.tsx @@ -29,24 +29,26 @@ export function SettingsContent(props: { }} > - + - - - props.title(props.list() as SettingsList, key) - } - navigate={(keys) => navigate(keys.join("/"))} - /> - + + + + props.title(props.list() as SettingsList, key) + } + navigate={(keys) => navigate(keys.join("/"))} + /> + + {props.children}
- + @@ -88,10 +90,6 @@ const InnerContent = styled("div", { padding: "80px 32px", justifyContent: "stretch", zIndex: 1, - - "@media (max-width: 800px)": { - padding: "12px" - } }, }); @@ -125,14 +123,10 @@ const CloseAction = styled("div", { marginTop: "4px", display: "flex", justifyContent: "center", - width: "36px", + width: "40px", fontWeight: 600, color: "var(--md-sys-color-on-surface)", fontSize: "0.75rem", }, - - "@media (max-width: 800px)": { - display: "none" - } }, }); diff --git a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx index 3aa3f08795..c366dbf088 100644 --- a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx +++ b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx @@ -38,7 +38,7 @@ export function SettingsSidebar(props: { return (
- + {list.prepend} @@ -123,10 +123,6 @@ const Content = styled("div", { "& a > div": { margin: 0, }, - - "@media (max-width: 800px)": { - padding: "12px 0" - } }, }); diff --git a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx index acb2528de5..9cfafa6686 100644 --- a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx +++ b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx @@ -7,9 +7,7 @@ export const SidebarButton = styled("a", { base: { // for : position: "relative", - minWidth: 0, - display: "flex", alignItems: "center", padding: "6px 8px", @@ -33,11 +31,6 @@ export const SidebarButton = styled("a", { background: "var(--md-sys-color-primary-container)", }, }, - mobileOnly: { - true: { - "@media (min-width: 800px)": {display: "none"} - } - } }, }); diff --git a/packages/client/components/app/interface/settings/user/_AccountCard.tsx b/packages/client/components/app/interface/settings/user/_AccountCard.tsx index f0ffdf0a81..f93b10faaa 100644 --- a/packages/client/components/app/interface/settings/user/_AccountCard.tsx +++ b/packages/client/components/app/interface/settings/user/_AccountCard.tsx @@ -43,11 +43,9 @@ export function AccountCard() { ); } -export function BackCard(props: { - onClose?: () => void; -}) { +export function BackCard(props: { onClose?: () => void }) { return ( - + @@ -57,4 +55,4 @@ export function BackCard(props: { ); -} \ No newline at end of file +} diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index eb9343d4b7..a47a87770f 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -5,6 +5,7 @@ import { Motion, Presence } from "solid-motionone"; import { Settings, SettingsConfigurations } from "@revolt/app"; import { DialogProps } from "@revolt/ui"; +import { styled } from "styled-system/jsx"; import { Modals } from "../types"; /** @@ -31,14 +32,7 @@ export function SettingsModal( > - - +
); } + +const Base = styled(Motion.div, { + base: { + height: "100%", + pointerEvents: "all", + display: "flex", + color: "var(--md-sys-color-on-surface)", + background: "var(--md-sys-color-surface-container-highest)", + + //Tablet view + "& .setMobileBack": { display: "none" }, + "@media (max-width: 800px)": { + "& .setCont": { padding: "12px" }, + "& .setSidebar": { padding: "12px 0" }, + "& .setClose": { display: "none" }, + "& .setMobileBack": { display: "flex" }, + }, + }, +}); From 1b07b542fd115e41e1f7916b9e04a3bc3fe38c0a Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 8 May 2026 04:35:36 -0400 Subject: [PATCH 06/34] fix: Move the CSS again & misc - I swear this is the last time lol. Now using classes for everything instead of just some of it, will also help with changes for phone view - Change threshold for tablet view to 900px Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../interface/settings/_layout/Content.tsx | 6 +--- .../interface/settings/_layout/Sidebar.tsx | 3 +- .../components/modal/modals/Settings.tsx | 25 ++-------------- packages/client/components/ui/styles.css | 29 +++++++++++++++++++ packages/client/src/Interface.tsx | 12 ++------ packages/client/src/interface/Sidebar.tsx | 2 +- .../navigation/channels/HomeSidebar.tsx | 2 +- .../navigation/channels/ServerSidebar.tsx | 5 +++- 8 files changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/client/components/app/interface/settings/_layout/Content.tsx b/packages/client/components/app/interface/settings/_layout/Content.tsx index efe91ef8c7..d1bbce27a1 100644 --- a/packages/client/components/app/interface/settings/_layout/Content.tsx +++ b/packages/client/components/app/interface/settings/_layout/Content.tsx @@ -23,11 +23,7 @@ export function SettingsContent(props: { const { navigate } = useSettingsNavigation(); return ( -
+
diff --git a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx index c366dbf088..1635b0a730 100644 --- a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx +++ b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx @@ -36,7 +36,7 @@ export function SettingsSidebar(props: { }); return ( - +
@@ -104,6 +104,7 @@ const Base = styled("div", { flex: "1 0 218px", paddingLeft: "8px", justifyContent: "flex-end", + height: "100%", }, }); diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index a47a87770f..9a14aae5ce 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -5,7 +5,6 @@ import { Motion, Presence } from "solid-motionone"; import { Settings, SettingsConfigurations } from "@revolt/app"; import { DialogProps } from "@revolt/ui"; -import { styled } from "styled-system/jsx"; import { Modals } from "../types"; /** @@ -32,7 +31,8 @@ export function SettingsModal( > - - +
); } - -const Base = styled(Motion.div, { - base: { - height: "100%", - pointerEvents: "all", - display: "flex", - color: "var(--md-sys-color-on-surface)", - background: "var(--md-sys-color-surface-container-highest)", - - //Tablet view - "& .setMobileBack": { display: "none" }, - "@media (max-width: 800px)": { - "& .setCont": { padding: "12px" }, - "& .setSidebar": { padding: "12px 0" }, - "& .setClose": { display: "none" }, - "& .setMobileBack": { display: "flex" }, - }, - }, -}); diff --git a/packages/client/components/ui/styles.css b/packages/client/components/ui/styles.css index 44fd0dccc0..f843a5a8b1 100644 --- a/packages/client/components/ui/styles.css +++ b/packages/client/components/ui/styles.css @@ -28,6 +28,35 @@ body { background: #191919; } +/* App: Desktop view */ +.appRoot { + display: flex; + flex-direction: column; + height: 100%; +} +.appSidebar { + display: flex; + flex-shrink: 0; +} + +/* Settings: Desktop view */ +.setMain { + display: flex; + height: 100%; + pointer-events: all; + color: var(--md-sys-color-on-surface); + background: var(--md-sys-color-surface-container-highest); +} + +/* Settings: Tablet view */ +.setMobileBack { display: none } +@media (max-width: 900px) { + .setCont { padding: 12px } + .setSidebar { padding: 8px 0 } + .setClose { display: none } + .setMobileBack { display: flex } +} + /* HighlightJs */ /*! Theme: GitHub Dark diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index 5efe073e01..f66af90b7f 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -1,4 +1,4 @@ -import { JSX, Match, Switch, createEffect } from "solid-js"; +import { createEffect, JSX, Match, Switch } from "solid-js"; import { Server } from "stoat.js"; import { styled } from "styled-system/jsx"; @@ -59,13 +59,7 @@ const Interface = (props: { children: JSX.Element }) => { return ( -
+
}> @@ -96,6 +90,7 @@ const Interface = (props: { children: JSX.Element }) => { })} /> +
{ }); return ( - +
diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index 507cc8ecc7..17576d8e83 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -190,7 +190,10 @@ export const ServerSidebar = (props: Props) => { } return ( - + From 7d9fb6d9bfb8e8a264d51c12b5ce8f0b68c33e33 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:01:43 -0500 Subject: [PATCH 07/34] feat: Mobile UI - Phone UI triggers at < 600px width - Animated, swipable drawer slider, inspired by Discord app Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../app/interface/settings/Settings.tsx | 4 + .../interface/settings/_layout/Content.tsx | 5 +- .../components/modal/modals/Settings.tsx | 18 +- .../ui/components/navigation/SlideDrawer.ts | 169 ++++++++++++++++++ packages/client/components/ui/styles.css | 26 +++ packages/client/src/Interface.tsx | 27 ++- 6 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 packages/client/components/ui/components/navigation/SlideDrawer.ts diff --git a/packages/client/components/app/interface/settings/Settings.tsx b/packages/client/components/app/interface/settings/Settings.tsx index 43e71a98a9..d7504d46c1 100644 --- a/packages/client/components/app/interface/settings/Settings.tsx +++ b/packages/client/components/app/interface/settings/Settings.tsx @@ -4,6 +4,7 @@ import { createContext, createMemo, createSignal, + Setter, untrack, useContext, } from "solid-js"; @@ -25,6 +26,8 @@ export interface SettingsProps { * Settings context */ context: never; + + contentRef: Setter; } /** @@ -98,6 +101,7 @@ export function Settings(props: SettingsProps & SettingsConfiguration) { <> >; title: (ctx: SettingsList, key: string) => string; page: Accessor; + ref: Setter; }) { const { navigate } = useSettingsNavigation(); return ( -
+
diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index 9a14aae5ce..38a67de4f0 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -1,10 +1,11 @@ -import { Show } from "solid-js"; +import { createEffect, createSignal, on, onCleanup, Show } from "solid-js"; import { Portal } from "solid-js/web"; import { Motion, Presence } from "solid-motionone"; import { Settings, SettingsConfigurations } from "@revolt/app"; import { DialogProps } from "@revolt/ui"; +import { SlideDrawer } from "@revolt/ui/components/navigation/SlideDrawer"; import { Modals } from "../types"; /** @@ -16,6 +17,19 @@ export function SettingsModal( // eslint-disable-next-line solid/reactivity const config = SettingsConfigurations[props.config]; + //Drawer slider for mobile + let rootRef, sDrawer: SlideDrawer | null; + const [contRef, setContRef] = createSignal(); + createEffect( + on(contRef, (cont) => { + if (cont) sDrawer = new SlideDrawer(cont, rootRef!); + }), + ); + onCleanup(() => { + sDrawer?.delete(); + sDrawer = null; + }); + return (
diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts new file mode 100644 index 0000000000..8f8072b9d9 --- /dev/null +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -0,0 +1,169 @@ +const ANIM_MS = 500, + TRIG_X = 20, + CANCEL_Y = 20; + +type TrackTouch = { + id: number; + x: number; + y: number; + newX?: number; + newT?: number; + prevX?: number; + prevT?: number; + trig?: boolean; +}; + +export class SlideDrawer { + enabled = false; + private media; + private touch: TrackTouch | null = null; + private tTmr: NodeJS.Timeout | null = null; + private ofs = 0; + + constructor( + private drawer: HTMLElement, + private root: HTMLElement, + ) { + console.log("INIT", drawer, root); + root.ontouchstart = this.start.bind(this); + root.ontouchmove = root.ontouchend = this.move.bind(this); + + //Auto-enable based on device width + const pwMax = getComputedStyle(document.body).getPropertyValue( + "--phone-max-width", + ); + this.media = matchMedia(`(max-width: ${pwMax}`); + this.media.onchange = (e) => this.setEnabled(e.matches); + this.setEnabled(this.media.matches); + } + + private start(e: TouchEvent) { + //Cancel if more than one finger + if (e.touches.length > 1) { + this.touch = null; + return; + } + if (this.touch || !this.enabled) return; + + //Track this touch + const t = e.touches[0]; + this.touch = { + id: t.identifier, + x: t.screenX, + y: t.screenY, + }; + + console.log("TSTART", this.touch.x, this.touch.y); + } + + private move(e: TouchEvent) { + if (!this.touch) return; + const isEnd = e.type === "touchend"; + let t, tNew; + for (t of e.changedTouches) + if (t.identifier === this.touch.id) { + tNew = t; + break; + } + if (!tNew) return; + + t = this.touch; + const dy = tNew.screenY - t.y, + ds = this.drawer.style, + max = -innerWidth; + let dx = tNew.screenX - t.x, + trig = t.trig; + + if (!trig && Math.abs(dy) > CANCEL_Y) { + console.log("CANCEL at Y", dy); + this.touch = null; + } else if (trig || Math.abs(dx) > TRIG_X) { + if (!trig) { + console.log("TRIG at X", dx); + t.trig = trig = true; + this.tfTimer(); + } + + dx = Math.max(Math.min(this.ofs + dx, 0), max); + ds.transform = `translateX(${dx}px)`; + e.preventDefault(); + e.stopPropagation(); + } + + if (isEnd) { + console.log("END at X", dx); + + //TODO: Calc avg velocity and smooth w/ moving avg or something + //If velocity at touchend is higher than threshold, + //overrides the show/hide result regardless of drawer position + + //Finalize show/hide state + if (trig) this.tfTimer(true, dx < max / 2); + this.touch = null; + } + } + + private tfTimer(set = false, show = false) { + //Animate transition + const ds = this.drawer.style; + this.setElState(false); + if (set) { + this.ofs = show ? -innerWidth : 0; + ds.transition = `transform ${ANIM_MS}ms`; + ds.transform = `translateX(${this.ofs}px)`; + } else { + ds.transition = ds.transform = ""; + } + + //Finalize after delay + clearTimeout(this.tTmr!); + this.tTmr = set + ? setTimeout(() => { + ds.transition = ds.transform = ""; + this.setElState(show); + this.tTmr = null; + }, ANIM_MS + 50) + : null; + } + + private setElState(show: boolean) { + const ds = this.drawer.style; + this.root.style.width = show ? "" : "200vw"; + ds.marginLeft = show ? "" : "100vw"; + } + + delete() { + console.log("DEL"); + this.setEnabled(false); + this.root.ontouchstart = + this.root.ontouchmove = + this.root.ontouchend = + this.media.onchange = + null; + } + + setEnabled(en: boolean) { + if (this.enabled !== en) { + this.drawer.style.zIndex = en ? "1" : ""; + this.tfTimer(); + this.touch = null; + if (!en) this.setElState(true); + this.ofs = 0; + } + this.enabled = en; + } + + isShown() { + return this.ofs !== 0; + } + + setShown(show: boolean) { + if (!this.enabled || this.touch?.trig || this.tTmr) return false; + if (this.isShown() !== show) { + this.setElState(false); + this.drawer.style.transform = `translateX(${this.ofs}px)`; + setTimeout(() => this.tfTimer(true, show), 0); + } + return true; + } +} diff --git a/packages/client/components/ui/styles.css b/packages/client/components/ui/styles.css index f843a5a8b1..0e1b2537e1 100644 --- a/packages/client/components/ui/styles.css +++ b/packages/client/components/ui/styles.css @@ -17,6 +17,7 @@ body { margin: 0; background: #191919; font-family: var(--fonts-primary); + --phone-max-width: 600px; } #root { @@ -39,6 +40,17 @@ body { flex-shrink: 0; } +/* Main: Phone view */ +@media (max-width: 600px) { + .appSbBase { flex-grow: 1 } + .appSidebar { + --layout-width-channel-sidebar: auto; + position: absolute; + width: 100vw; + height: 100%; + } +} + /* Settings: Desktop view */ .setMain { display: flex; @@ -57,6 +69,20 @@ body { .setMobileBack { display: flex } } +/* Settings: Phone view */ +@media (max-width: 600px) { + .setSbBase { + position: absolute; + width: 100vw; + height: 100%; + padding-left: 12px; + } + .setSbBase > * { width: 100% } + .setSidebar { max-width: unset } + .setBase { border-radius: 0 } + .setCont { height: 100vh } +} + /* HighlightJs */ /*! Theme: GitHub Dark diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index f66af90b7f..72b501eede 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -1,4 +1,12 @@ -import { createEffect, JSX, Match, Switch } from "solid-js"; +import { + createEffect, + createSignal, + JSX, + Match, + on, + onCleanup, + Switch, +} from "solid-js"; import { Server } from "stoat.js"; import { styled } from "styled-system/jsx"; @@ -15,6 +23,7 @@ import { useState } from "@revolt/state"; import { LAYOUT_SECTIONS } from "@revolt/state/stores/Layout"; import { CircularProgress } from "@revolt/ui"; +import { SlideDrawer } from "../components/ui/components/navigation/SlideDrawer"; import { Sidebar } from "./interface/Sidebar"; /** @@ -57,9 +66,22 @@ const Interface = (props: { children: JSX.Element }) => { ].includes(lifecycle.state()); } + //Drawer slider for mobile + let rootRef, sDrawer: SlideDrawer | null; + const [contRef, setContRef] = createSignal(); + createEffect( + on(contRef, (cont) => { + if (cont) sDrawer = new SlideDrawer(cont, rootRef!); + }), + ); + onCleanup(() => { + sDrawer?.delete(); + sDrawer = null; + }); + return ( -
+
}> @@ -90,6 +112,7 @@ const Interface = (props: { children: JSX.Element }) => { })} /> Date: Tue, 17 Feb 2026 21:13:32 -0500 Subject: [PATCH 08/34] feat: Slide that SlideDrawer real smooth-like - Inertia-based snapping for SlideDrawer! We mustelids are experts in speed so this was inevitable - Fix bug that could cause a SlideDrawer race condition if openning and closing settings menu super fast - Add appDrawer to app state to update things when the state of the UI changes between mobile/non-mobile - Don't trigger floating tooltops when input is touched, otherwise they get stuck in a showing state on touchscreens - Reset layout to member sidebar being hidden when mobile view is shown (otherwise how to close it seems to cause some confusion among those who helped test so far) - Replace primary sidebar collapse button with a "back" button for mobie view Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../components/modal/modals/Settings.tsx | 2 +- packages/client/components/state/index.tsx | 8 ++ .../ui/components/floating/Tooltip.tsx | 8 +- .../ui/components/navigation/SlideDrawer.ts | 93 ++++++++++++++----- .../components/ui/directives/floating.ts | 13 ++- packages/client/src/Interface.tsx | 19 ++-- .../src/interface/common/CommonHeader.tsx | 29 +++++- 7 files changed, 131 insertions(+), 41 deletions(-) diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index 38a67de4f0..ecee456096 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -22,7 +22,7 @@ export function SettingsModal( const [contRef, setContRef] = createSignal(); createEffect( on(contRef, (cont) => { - if (cont) sDrawer = new SlideDrawer(cont, rootRef!); + if (cont && !sDrawer) sDrawer = new SlideDrawer(cont, rootRef!); }), ); onCleanup(() => { diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx index f061815f98..29e1586bef 100644 --- a/packages/client/components/state/index.tsx +++ b/packages/client/components/state/index.tsx @@ -12,6 +12,7 @@ import equal from "fast-deep-equal"; import localforage from "localforage"; import { isMobileBrowser } from "@livekit/components-core"; +import { SlideDrawer } from "@revolt/ui/components/navigation/SlideDrawer"; import { AbstractStore, Store } from "./stores"; import { Auth } from "./stores/Auth"; import { Draft } from "./stores/Draft"; @@ -51,7 +52,10 @@ export class State { private store: Store; private setStore: SetStoreFunction; private writeQueue: Record; + isMobile: boolean; + appDrawer; + setAppDrawer; // define all stores auth = new Auth(this); @@ -108,6 +112,10 @@ export class State { this.setStore = setStore; this.writeQueue = {}; this.isMobile = isMobileBrowser(); + + const [ad, setAd] = createSignal(); + this.appDrawer = ad; + this.setAppDrawer = setAd; } /** diff --git a/packages/client/components/ui/components/floating/Tooltip.tsx b/packages/client/components/ui/components/floating/Tooltip.tsx index c8228e6e66..2b212d3dcc 100644 --- a/packages/client/components/ui/components/floating/Tooltip.tsx +++ b/packages/client/components/ui/components/floating/Tooltip.tsx @@ -35,12 +35,6 @@ export function Tooltip(props: Props) { const [local, remote] = splitProps(props, ["children"]); return ( -
- {local.children} -
+
{local.children}
); } diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index 8f8072b9d9..a38c1415c4 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -1,4 +1,7 @@ -const ANIM_MS = 500, +const ANIM_MS = 200, + VEL_MS = 33, //30Hz velocity update + VEL_AVG = 5, //Moving avg smoothing + VEL_TRIG = 0.5, //Override drawer pos if over TRIG_X = 20, CANCEL_Y = 20; @@ -11,6 +14,8 @@ type TrackTouch = { prevX?: number; prevT?: number; trig?: boolean; + vAvg?: Array; + vOfs?: number; }; export class SlideDrawer { @@ -18,15 +23,21 @@ export class SlideDrawer { private media; private touch: TrackTouch | null = null; private tTmr: NodeJS.Timeout | null = null; + private vTmr: NodeJS.Timeout | null = null; private ofs = 0; constructor( private drawer: HTMLElement, private root: HTMLElement, + public onStateChanged: ((en: boolean) => void) | null = null, ) { console.log("INIT", drawer, root); - root.ontouchstart = this.start.bind(this); - root.ontouchmove = root.ontouchend = this.move.bind(this); + + this.start = this.start.bind(this); + this.move = this.move.bind(this); + root.addEventListener("touchstart", this.start); + root.addEventListener("touchmove", this.move); + root.addEventListener("touchend", this.move); //Auto-enable based on device width const pwMax = getComputedStyle(document.body).getPropertyValue( @@ -39,10 +50,7 @@ export class SlideDrawer { private start(e: TouchEvent) { //Cancel if more than one finger - if (e.touches.length > 1) { - this.touch = null; - return; - } + if (e.touches.length > 1) return this.endTouch(); if (this.touch || !this.enabled) return; //Track this touch @@ -76,12 +84,18 @@ export class SlideDrawer { if (!trig && Math.abs(dy) > CANCEL_Y) { console.log("CANCEL at Y", dy); - this.touch = null; + this.endTouch(); } else if (trig || Math.abs(dx) > TRIG_X) { + //Store new/prev X for vel calc + const type = trig ? "new" : "prev"; + t[`${type}X`] = dx; + t[`${type}T`] = performance.now(); + if (!trig) { console.log("TRIG at X", dx); t.trig = trig = true; this.tfTimer(); + this.velTimer(); } dx = Math.max(Math.min(this.ofs + dx, 0), max); @@ -92,15 +106,52 @@ export class SlideDrawer { if (isEnd) { console.log("END at X", dx); + if (trig) { + //Calc avg vel + let vel = 0; + if (t.vAvg) { + let v; + for (v of t.vAvg) vel += v; + vel /= t.vAvg.length; + } + //Finalize show/hide state + let show = dx < max / 2; + if (vel > VEL_TRIG) show = false; + else if (vel < -VEL_TRIG) show = true; + this.tfTimer(true, show); + } + this.endTouch(); + } + } - //TODO: Calc avg velocity and smooth w/ moving avg or something - //If velocity at touchend is higher than threshold, - //overrides the show/hide result regardless of drawer position + private velTimer() { + if (this.vTmr) return; + this.vTmr = setInterval(() => { + const t = this.touch; + if (!t || !t.newT) return; + + //Velocity since last update + const stale = t.prevT === t.newT, + vel = stale ? 0 : (t.newX! - t.prevX!) / (t.newT! - t.prevT!); + + t.prevX = t.newX; + t.prevT = t.newT; + + if (t.vAvg) { + //Insert at vOfs + t.vAvg[t.vOfs!] = vel; + if (++t.vOfs! === VEL_AVG) t.vOfs = 0; + } else { + //Fill with first data + t.vAvg = [vel]; + t.vOfs = 1; + } + }, VEL_MS); + } - //Finalize show/hide state - if (trig) this.tfTimer(true, dx < max / 2); - this.touch = null; - } + private endTouch() { + clearInterval(this.vTmr!); + this.touch = this.vTmr = null; } private tfTimer(set = false, show = false) { @@ -135,22 +186,22 @@ export class SlideDrawer { delete() { console.log("DEL"); this.setEnabled(false); - this.root.ontouchstart = - this.root.ontouchmove = - this.root.ontouchend = - this.media.onchange = - null; + this.root.removeEventListener("touchstart", this.start); + this.root.removeEventListener("touchmove", this.move); + this.root.removeEventListener("touchend", this.move); + this.media.onchange = null; } setEnabled(en: boolean) { if (this.enabled !== en) { this.drawer.style.zIndex = en ? "1" : ""; this.tfTimer(); - this.touch = null; + this.endTouch(); if (!en) this.setElState(true); this.ofs = 0; } this.enabled = en; + if (this.onStateChanged) this.onStateChanged(en); } isShown() { diff --git a/packages/client/components/ui/directives/floating.ts b/packages/client/components/ui/directives/floating.ts index d745be7ebf..3171623d8c 100644 --- a/packages/client/components/ui/directives/floating.ts +++ b/packages/client/components/ui/directives/floating.ts @@ -125,11 +125,13 @@ export function floating(element: HTMLElement, accessor: Accessor) { trigger("contextMenu"); } + let isTouch = false; + /** * Handle mouse entering */ function onMouseEnter() { - trigger("tooltip", true); + if (!isTouch) trigger("tooltip", true); } /** @@ -139,6 +141,11 @@ export function floating(element: HTMLElement, accessor: Accessor) { trigger("tooltip", false); } + function onTouch() { + isTouch = true; + setTimeout(() => (isTouch = false), 100); + } + createEffect( on( () => accessor().userCard, @@ -166,10 +173,14 @@ export function floating(element: HTMLElement, accessor: Accessor) { element.addEventListener("mouseenter", onMouseEnter); element.addEventListener("mouseleave", onMouseLeave); + element.addEventListener("touchstart", onTouch); + element.addEventListener("touchend", onTouch); onCleanup(() => { element.removeEventListener("mouseenter", onMouseEnter); element.removeEventListener("mouseleave", onMouseLeave); + element.addEventListener("touchstart", onTouch); + element.addEventListener("touchend", onTouch); }); } }, diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index 72b501eede..22dc07f824 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -40,10 +40,7 @@ const Interface = (props: { children: JSX.Element }) => { if (!e.defaultPrevented) { if (e.to === "/settings") { e.preventDefault(); - openModal({ - type: "settings", - config: "user", - }); + openModal({ type: "settings", config: "user" }); } else if (typeof e.to === "string") { state.layout.setLastActivePath(e.to); } @@ -69,14 +66,24 @@ const Interface = (props: { children: JSX.Element }) => { //Drawer slider for mobile let rootRef, sDrawer: SlideDrawer | null; const [contRef, setContRef] = createSignal(); + function rstLayout() { + state.layout.setSectionState(LAYOUT_SECTIONS.PRIMARY_SIDEBAR, false, false); + state.layout.setSectionState(LAYOUT_SECTIONS.MEMBER_SIDEBAR, false, true); + } createEffect( on(contRef, (cont) => { - if (cont) sDrawer = new SlideDrawer(cont, rootRef!); + if (!cont || sDrawer) return; + sDrawer = new SlideDrawer(cont, rootRef!, (en) => { + setTimeout(() => { + state.setAppDrawer(en ? sDrawer : null); + if (en) rstLayout(); + }, 1); + }); }), ); onCleanup(() => { sDrawer?.delete(); - sDrawer = null; + state.setAppDrawer((sDrawer = null)); }); return ( diff --git a/packages/client/src/interface/common/CommonHeader.tsx b/packages/client/src/interface/common/CommonHeader.tsx index 3659db902b..27235f3df5 100644 --- a/packages/client/src/interface/common/CommonHeader.tsx +++ b/packages/client/src/interface/common/CommonHeader.tsx @@ -1,6 +1,9 @@ import { BiRegularChevronLeft, BiRegularChevronRight } from "solid-icons/bi"; + import { JSX, Match, Switch } from "solid-js"; +import MdArrowBack from "@material-design-icons/svg/outlined/arrow_back.svg?component-solid"; + import { useLingui } from "@lingui-solid/solid/macro"; import { css } from "styled-system/css"; @@ -19,9 +22,15 @@ export function HeaderIcon(props: { children: JSX.Element }) { return (
- state.layout.toggleSectionState(LAYOUT_SECTIONS.PRIMARY_SIDEBAR, true) - } + onClick={() => { + const ad = state.appDrawer(); + if (ad) ad.setShown(false); + else + state.layout.toggleSectionState( + LAYOUT_SECTIONS.PRIMARY_SIDEBAR, + true, + ); + }} use:floating={{ tooltip: { placement: "bottom", @@ -29,7 +38,17 @@ export function HeaderIcon(props: { children: JSX.Element }) { }, }} > - }> + + + {props.children} + + } + > + + + + {props.children} - {props.children}
); } From 40af88e70ae22dfbaced9e2622327e68dd9964e2 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:13:51 -0500 Subject: [PATCH 09/34] fix: Enable mobile autocorrect and autocapitalize on text input Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/features/texteditor/TextEditor2.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx index c955d7db59..05e3582948 100644 --- a/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx +++ b/packages/client/components/ui/components/features/texteditor/TextEditor2.tsx @@ -88,7 +88,7 @@ export function TextEditor2(props: Props) { const enterKeymap = keymap.of([ { key: isMobile ? "Ctrl-Enter" : "Enter", - //TODO: This is not working right, mobile seems to not trigger the function + //TODO Ctrl-Enter is only detected on Firefox mobile, not Chrome mobile run: (view) => { if (!props.onComplete) return false; @@ -126,7 +126,11 @@ export function TextEditor2(props: Props) { doc: props.initialValue?.[0], extensions: [ /* Enable browser spellchecking */ - EditorView.contentAttributes.of({ spellcheck: "true" }), + EditorView.contentAttributes.of({ + spellcheck: "true", + autocorrect: "true", + autocapitalize: "true", + }), /* Mount keymaps */ enterKeymap, From f52bfc8e0daa153f79f1a669601358a8dd495cba Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:58:39 -0500 Subject: [PATCH 10/34] fix: Un-round them corners for portrait phone view Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/ui/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/client/components/ui/styles.css b/packages/client/components/ui/styles.css index 0e1b2537e1..177b789420 100644 --- a/packages/client/components/ui/styles.css +++ b/packages/client/components/ui/styles.css @@ -42,6 +42,10 @@ body { /* Main: Phone view */ @media (max-width: 600px) { + main { + margin: 0; + border-radius: 0; + } .appSbBase { flex-grow: 1 } .appSidebar { --layout-width-channel-sidebar: auto; From 512f23b0b0bf833a075e41a83b5aefec5c0fb656 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:30:48 -0400 Subject: [PATCH 11/34] fix: Refine SlideDrawer inertia detection by always recalculating final velocity at end - Fix message context menu appearing in editor - Enable PWA for dev build for testing Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../app/interface/channels/text/Message.tsx | 6 ++- .../ui/components/navigation/SlideDrawer.ts | 53 ++++++++++--------- packages/client/vite.config.ts | 3 ++ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/Message.tsx b/packages/client/components/app/interface/channels/text/Message.tsx index 1022c1bcd1..15d24d2cfb 100644 --- a/packages/client/components/app/interface/channels/text/Message.tsx +++ b/packages/client/components/app/interface/channels/text/Message.tsx @@ -135,7 +135,11 @@ export function Message(props: Props) { />
} - contextMenu={() => } + contextMenu={() => + props.editing ? undefined : ( + + ) + } timestamp={props.message.createdAt} edited={props.message.editedAt} mentioned={props.message.mentioned} diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index a38c1415c4..43a1e7f300 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -1,8 +1,8 @@ const ANIM_MS = 200, VEL_MS = 33, //30Hz velocity update VEL_AVG = 5, //Moving avg smoothing - VEL_TRIG = 0.5, //Override drawer pos if over - TRIG_X = 20, + VEL_TRIG = 0.3, //Override drawer pos if over + TRIG_X = 10, CANCEL_Y = 20; type TrackTouch = { @@ -105,8 +105,8 @@ export class SlideDrawer { } if (isEnd) { - console.log("END at X", dx); if (trig) { + this.addVel(); //Calc avg vel let vel = 0; if (t.vAvg) { @@ -119,6 +119,7 @@ export class SlideDrawer { if (vel > VEL_TRIG) show = false; else if (vel < -VEL_TRIG) show = true; this.tfTimer(true, show); + console.log("END at X", dx, vel); } this.endTouch(); } @@ -126,27 +127,31 @@ export class SlideDrawer { private velTimer() { if (this.vTmr) return; - this.vTmr = setInterval(() => { - const t = this.touch; - if (!t || !t.newT) return; - - //Velocity since last update - const stale = t.prevT === t.newT, - vel = stale ? 0 : (t.newX! - t.prevX!) / (t.newT! - t.prevT!); - - t.prevX = t.newX; - t.prevT = t.newT; - - if (t.vAvg) { - //Insert at vOfs - t.vAvg[t.vOfs!] = vel; - if (++t.vOfs! === VEL_AVG) t.vOfs = 0; - } else { - //Fill with first data - t.vAvg = [vel]; - t.vOfs = 1; - } - }, VEL_MS); + this.vTmr = setInterval(this.addVel.bind(this), VEL_MS); + } + + private addVel(skipStale = false) { + const t = this.touch; + if (!t || !t.newT) return; + + //Velocity since last update + const stale = t.prevT === t.newT, + vel = stale ? 0 : (t.newX! - t.prevX!) / (t.newT! - t.prevT!); + + if (stale && skipStale) return; + + t.prevX = t.newX; + t.prevT = t.newT; + + if (t.vAvg) { + //Insert at vOfs + t.vAvg[t.vOfs!] = vel; + if (++t.vOfs! === VEL_AVG) t.vOfs = 0; + } else { + //Fill with first data + t.vAvg = [vel]; + t.vOfs = 1; + } } private endTouch() { diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index e1a9078935..2e502c1b3a 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -33,6 +33,9 @@ export default defineConfig({ injectManifest: { maximumFileSizeToCacheInBytes: 4000000, }, + devOptions: { + enabled: true, + }, manifest: { name: "Stoat", short_name: "Stoat", From d14e046bba61f9ceea291321a8dcdbfd196e5488 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 21 Feb 2026 15:20:47 -0500 Subject: [PATCH 12/34] fix: Sensible margains for modals on mobile to prevent weird scroll behavior - On mobile, skip UserCard preview (which doesn't look right on small displays) when tapping user icon and go straight to full UserProfile modal Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/design/Dialog.tsx | 1 + .../ui/components/floating/UserCard.tsx | 57 +++++++++++-------- packages/client/components/ui/styles.css | 1 + 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/client/components/ui/components/design/Dialog.tsx b/packages/client/components/ui/components/design/Dialog.tsx index b7b30f65e5..4ee2eda5d6 100644 --- a/packages/client/components/ui/components/design/Dialog.tsx +++ b/packages/client/components/ui/components/design/Dialog.tsx @@ -43,6 +43,7 @@ export function Dialog(props: Props) { void }, ) { + const { isMobile } = useState(); const { openModal } = useModals(); const query = useQuery(() => ({ queryKey: ["profile", props.user.id], queryFn: () => props.user.fetchProfile(), })); - function openFull() { + function openProfile() { openModal({ type: "user_profile", user: props.user }); + } + function openFull() { + openProfile(); props.onClose(); } return ( -
{ - e.preventDefault(); - e.stopImmediatePropagation(); - }} - > - - + +
{ + e.preventDefault(); + e.stopImmediatePropagation(); + }} + > + + - - - - - - - -
+ + + + + + +
+
+ ); } diff --git a/packages/client/components/ui/styles.css b/packages/client/components/ui/styles.css index 177b789420..567611e28f 100644 --- a/packages/client/components/ui/styles.css +++ b/packages/client/components/ui/styles.css @@ -46,6 +46,7 @@ body { margin: 0; border-radius: 0; } + .dialogScrim { padding: 30px } .appSbBase { flex-grow: 1 } .appSidebar { --layout-width-channel-sidebar: auto; From 437fbb922fedf55778639980dd8533e62bca77b9 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 22 Feb 2026 02:37:37 -0500 Subject: [PATCH 13/34] fix: Small chance that two modals have the same ID using Math.random for ID Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/modal/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/components/modal/index.tsx b/packages/client/components/modal/index.tsx index 9c919082ae..0719b2178f 100644 --- a/packages/client/components/modal/index.tsx +++ b/packages/client/components/modal/index.tsx @@ -61,11 +61,11 @@ export class ModalController { * @param props Modal parameters */ openModal(props: Modals) { - const id = Math.random().toString(); + //Unique ID from clock that can't run backwards + const id = performance.now().toString(); this.setModals((modals) => [ ...modals, { - // just need something unique id, show: true, props, From 87c600b2cb660fa2a1af0e9d35b1db752e089804 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:18:51 -0500 Subject: [PATCH 14/34] fix: Clicking channels/DMs/settings buttons opens slide drawer on mobile Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../settings/_layout/SidebarButton.tsx | 20 +- .../app/interface/settings/index.tsx | 5 +- .../interface/settings/user/_AccountCard.tsx | 2 +- .../components/modal/modals/Settings.tsx | 8 +- packages/client/components/state/index.tsx | 6 + .../ui/components/design/MenuButton.tsx | 69 ++++- .../navigation/channels/HomeSidebar.tsx | 288 +++++++++--------- .../navigation/channels/ServerSidebar.tsx | 147 +++++---- 8 files changed, 299 insertions(+), 246 deletions(-) diff --git a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx index 9cfafa6686..8cac2b5523 100644 --- a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx +++ b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx @@ -1,9 +1,27 @@ +import { useState } from "@revolt/state"; +import { JSX, splitProps } from "solid-js"; import { styled } from "styled-system/jsx"; /** * Sidebar button */ -export const SidebarButton = styled("a", { +export function SidebarButton( + props: JSX.HTMLAttributes & { noDrawer?: boolean }, +) { + const { diagDrawer } = useState(); + const [local, other] = splitProps(props, ["onClick"]); + + function onClick(e: Event) { + if (!props.noDrawer) diagDrawer()?.setShown(true); + // @ts-expect-error callable listener + if (local.onClick) local.onClick(e); + } + + // @ts-expect-error todo dunno about this error + return ; +} + +const SidebarButtonBase = styled("a", { base: { // for : position: "relative", diff --git a/packages/client/components/app/interface/settings/index.tsx b/packages/client/components/app/interface/settings/index.tsx index 82be8caf6c..d41573f794 100644 --- a/packages/client/components/app/interface/settings/index.tsx +++ b/packages/client/components/app/interface/settings/index.tsx @@ -12,10 +12,7 @@ export type SettingsConfiguration = { * @param props State information * @returns List */ - list: ( - context: T, - onClose?: () => void - ) => SettingsList; + list: (context: T, onClose?: () => void) => SettingsList; /** * Render the title of the current breadcrumb key diff --git a/packages/client/components/app/interface/settings/user/_AccountCard.tsx b/packages/client/components/app/interface/settings/user/_AccountCard.tsx index f93b10faaa..e3fac36757 100644 --- a/packages/client/components/app/interface/settings/user/_AccountCard.tsx +++ b/packages/client/components/app/interface/settings/user/_AccountCard.tsx @@ -45,7 +45,7 @@ export function AccountCard() { export function BackCard(props: { onClose?: () => void }) { return ( - + diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index ecee456096..39f9b91e0e 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -5,6 +5,7 @@ import { Motion, Presence } from "solid-motionone"; import { Settings, SettingsConfigurations } from "@revolt/app"; import { DialogProps } from "@revolt/ui"; +import { useState } from "@revolt/state"; import { SlideDrawer } from "@revolt/ui/components/navigation/SlideDrawer"; import { Modals } from "../types"; @@ -14,6 +15,7 @@ import { Modals } from "../types"; export function SettingsModal( props: DialogProps & Modals & { type: "settings" }, ) { + const { setDiagDrawer } = useState(); // eslint-disable-next-line solid/reactivity const config = SettingsConfigurations[props.config]; @@ -22,12 +24,14 @@ export function SettingsModal( const [contRef, setContRef] = createSignal(); createEffect( on(contRef, (cont) => { - if (cont && !sDrawer) sDrawer = new SlideDrawer(cont, rootRef!); + if (!cont || sDrawer) return; + sDrawer = new SlideDrawer(cont, rootRef!); + setDiagDrawer(sDrawer); }), ); onCleanup(() => { sDrawer?.delete(); - sDrawer = null; + setDiagDrawer((sDrawer = null)); }); return ( diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx index 29e1586bef..6f6db4dd0a 100644 --- a/packages/client/components/state/index.tsx +++ b/packages/client/components/state/index.tsx @@ -56,6 +56,8 @@ export class State { isMobile: boolean; appDrawer; setAppDrawer; + diagDrawer; + setDiagDrawer; // define all stores auth = new Auth(this); @@ -116,6 +118,10 @@ export class State { const [ad, setAd] = createSignal(); this.appDrawer = ad; this.setAppDrawer = setAd; + + const [dd, setDd] = createSignal(); + this.diagDrawer = dd; + this.setDiagDrawer = setDd; } /** diff --git a/packages/client/components/ui/components/design/MenuButton.tsx b/packages/client/components/ui/components/design/MenuButton.tsx index 29d18a87e3..37f24d4474 100644 --- a/packages/client/components/ui/components/design/MenuButton.tsx +++ b/packages/client/components/ui/components/design/MenuButton.tsx @@ -3,6 +3,7 @@ import { JSX, Show, splitProps } from "solid-js"; import { cva } from "styled-system/css"; import { styled } from "styled-system/jsx"; +import { useState } from "@revolt/state"; import { Ripple } from "./Ripple"; import { Unreads } from "./Unreads"; @@ -43,8 +44,14 @@ export type Props = { /** * Button intended for sidebar contexts */ -export function MenuButton(props: Props & JSX.HTMLAttributes) { +export function MenuButton( + props: Props & + JSX.HTMLAttributes & + JSX.HTMLAttributes & { href?: string }, +) { + const { appDrawer } = useState(); const [local, other] = splitProps(props, [ + "onClick", "attention", "size", "icon", @@ -53,20 +60,15 @@ export function MenuButton(props: Props & JSX.HTMLAttributes) { "actions", ]); - return ( - // TODO: port to panda-css to merge down components -
+ function onClick(e: Event) { + appDrawer()?.setShown(true); + // @ts-expect-error callable listener + if (local.onClick) local.onClick(e); + } + + const cont = ( + <> - {/* */} {local.icon} {local.children} @@ -83,8 +85,43 @@ export function MenuButton(props: Props & JSX.HTMLAttributes) { {local.actions} )} - {/* */} -
+ + ); + + return ( + // TODO: port to panda-css to merge down components + + {cont} +
+ } + > + + {cont} + + ); } diff --git a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx index d07477aa73..82879e7e9a 100644 --- a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx @@ -75,37 +75,33 @@ export const HomeSidebar = (props: Props) => { Conversations - - home} - attention={location.pathname === "/app" ? "selected" : "normal"} - > - - Home - - - + home} + attention={location.pathname === "/app" ? "selected" : "normal"} + > + + Home + +
- - group} - attention={ - location.pathname === "/friends" ? "selected" : "normal" - } - > - - Friends -
- - {pendingRequests()} requests - - - - + group} + attention={location.pathname === "/friends" ? "selected" : "normal"} + > + + Friends +
+ + {pendingRequests()} requests + + +
@@ -124,30 +120,27 @@ export const HomeSidebar = (props: Props) => { } > - - note_stack} - attention={ - props.channelId && savedNotesChannelId() === props.channelId - ? "selected" - : "normal" - } - > - - Saved Notes - - - + note_stack} + attention={ + props.channelId && savedNotesChannelId() === props.channelId + ? "selected" + : "normal" + } + > + + Saved Notes + + Direct Messages openModal({ type: "create_group", @@ -185,7 +178,6 @@ export const HomeSidebar = (props: Props) => { // @ts-expect-error missing type on Entry role="listitem" tabIndex={item.tabIndex} - style={item.style} channel={item.item} active={item.item.id === props.channelId} isMobile={isMobile} @@ -291,111 +283,111 @@ function Entry( ); return ( - - - - - - - - } - /> - - - } - actions={ - - { - e.preventDefault(); - openModal({ - type: "delete_channel", - channel: local.channel, - }); - }} - > - - - - } - use:floating={{ - contextMenu: () => - local.channel.type === "DirectMessage" ? ( - - ) : ( - - ), - }} - > - - - - - - - - {/* + + + + + + } + /> + + + } + actions={ + + { + e.preventDefault(); + openModal({ + type: "delete_channel", + channel: local.channel, + }); + }} + > + + + + } + use:floating={{ + contextMenu: () => + local.channel.type === "DirectMessage" ? ( + + ) : ( + + ), + }} + > + + + + + + + + {/* */} - {local.channel.recipientIds.size}{" "} - {local.channel.recipientIds.size > 1 ? `Members` : "Member"} - - - - - {local.channel?.recipient?.displayName} - - - } - placement="top-start" - aria={status()!} - > - - - - - - - - - - + {local.channel.recipientIds.size}{" "} + {local.channel.recipientIds.size > 1 ? `Members` : "Member"} + + + + + {local.channel?.recipient?.displayName} + + + } + placement="top-start" + aria={status()!} + > + + + + + + + + + ); } diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index 17576d8e83..766ded06b4 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -484,82 +484,81 @@ function Entry( ); return ( - - - - grid_3x3}> - - - headset_mic - - - - - - - - } - actions={ - - - { - e.preventDefault(); - openModal({ - type: "create_invite", - channel: props.channel, - }); - }} - > - - person_add - - - - - { - e.preventDefault(); - openModal({ - type: "settings", - config: "channel", - context: props.channel, - }); - }} + + + grid_3x3}> + + - - settings - - - + headset_mic + + + + + - } - > - - - - - - - - + + } + actions={ + + + { + e.preventDefault(); + openModal({ + type: "create_invite", + channel: props.channel, + }); + }} + > + + person_add + + + + + { + e.preventDefault(); + openModal({ + type: "settings", + config: "channel", + context: props.channel, + }); + }} + > + + settings + + + + + } + > + + + + + + + ); } From cc95f3e124da2d28e4b9fda8f366d727781bf741 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:59:50 -0500 Subject: [PATCH 15/34] fix: Cease this madness - Fix #883 text overflow out of dialog modals - Remove old "error" type modal - Fix invite embed going past edge of screen on mobile Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/modal/types.ts | 9 --------- packages/client/components/ui/components/design/Text.tsx | 3 +++ .../ui/components/features/messaging/elements/Invite.tsx | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/client/components/modal/types.ts b/packages/client/components/modal/types.ts index 381ab0140c..28d35fdf57 100644 --- a/packages/client/components/modal/types.ts +++ b/packages/client/components/modal/types.ts @@ -152,15 +152,6 @@ export type Modals = type: "emoji_preview"; emoji: Emoji; } - | { - /** - * @deprecated build proper error handling! - */ - type: "error"; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error: any; - } | { type: "error2"; diff --git a/packages/client/components/ui/components/design/Text.tsx b/packages/client/components/ui/components/design/Text.tsx index 74472f7fd2..3134da8360 100644 --- a/packages/client/components/ui/components/design/Text.tsx +++ b/packages/client/components/ui/components/design/Text.tsx @@ -143,6 +143,7 @@ export const typography = cva({ class: "body", size: "large", css: { + overflowWrap: "anywhere", lineHeight: "1.5rem", fontSize: "1rem", letterSpacing: "0.009375rem", @@ -153,6 +154,7 @@ export const typography = cva({ class: "body", size: "medium", css: { + overflowWrap: "anywhere", lineHeight: "1.25rem", fontSize: "0.875rem", letterSpacing: "0.015625rem", @@ -163,6 +165,7 @@ export const typography = cva({ class: "body", size: "small", css: { + overflowWrap: "anywhere", lineHeight: "1rem", fontSize: "0.75rem", letterSpacing: "0.025rem", diff --git a/packages/client/components/ui/components/features/messaging/elements/Invite.tsx b/packages/client/components/ui/components/features/messaging/elements/Invite.tsx index f9b8b1660a..2f1a72e1a1 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Invite.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Invite.tsx @@ -97,7 +97,7 @@ const Base = styled("div", { display: "flex", alignItems: "center", - width: "320px", + maxWidth: "320px", height: "64px", gap: "var(--gap-md)", padding: "var(--gap-md)", From 0571b709ee6af549fe69a1ef7c5733217986ff79 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:37:49 -0500 Subject: [PATCH 16/34] fix: Disable context menu when editing message, so you can accept OS spellcheck suggestions - Remove debug print from SlideDrawer Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../components/app/interface/channels/text/Message.tsx | 8 ++++---- .../components/ui/components/navigation/SlideDrawer.ts | 8 -------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/Message.tsx b/packages/client/components/app/interface/channels/text/Message.tsx index 15d24d2cfb..0471e0d949 100644 --- a/packages/client/components/app/interface/channels/text/Message.tsx +++ b/packages/client/components/app/interface/channels/text/Message.tsx @@ -135,10 +135,10 @@ export function Message(props: Props) { />
} - contextMenu={() => - props.editing ? undefined : ( - - ) + contextMenu={ + props.editing + ? undefined + : () => } timestamp={props.message.createdAt} edited={props.message.editedAt} diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index 43a1e7f300..40b4439c57 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -31,8 +31,6 @@ export class SlideDrawer { private root: HTMLElement, public onStateChanged: ((en: boolean) => void) | null = null, ) { - console.log("INIT", drawer, root); - this.start = this.start.bind(this); this.move = this.move.bind(this); root.addEventListener("touchstart", this.start); @@ -60,8 +58,6 @@ export class SlideDrawer { x: t.screenX, y: t.screenY, }; - - console.log("TSTART", this.touch.x, this.touch.y); } private move(e: TouchEvent) { @@ -83,7 +79,6 @@ export class SlideDrawer { trig = t.trig; if (!trig && Math.abs(dy) > CANCEL_Y) { - console.log("CANCEL at Y", dy); this.endTouch(); } else if (trig || Math.abs(dx) > TRIG_X) { //Store new/prev X for vel calc @@ -92,7 +87,6 @@ export class SlideDrawer { t[`${type}T`] = performance.now(); if (!trig) { - console.log("TRIG at X", dx); t.trig = trig = true; this.tfTimer(); this.velTimer(); @@ -119,7 +113,6 @@ export class SlideDrawer { if (vel > VEL_TRIG) show = false; else if (vel < -VEL_TRIG) show = true; this.tfTimer(true, show); - console.log("END at X", dx, vel); } this.endTouch(); } @@ -189,7 +182,6 @@ export class SlideDrawer { } delete() { - console.log("DEL"); this.setEnabled(false); this.root.removeEventListener("touchstart", this.start); this.root.removeEventListener("touchmove", this.move); From 476763af5932d4721696d705cabfe548dd3c148b Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:20:50 -0500 Subject: [PATCH 17/34] feat: Adding a react button to the context menu was harder than anticipated - Also streamlined the Media Picker menu in the process, should now be much more efficent by generating a lot less stray DOM elements, and using only one Media Picker instance per message - Only show member sidebar when in server or group DM, because it's blank in normal DMs which causes confusion Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../app/interface/channels/text/Message.tsx | 68 +++++++++++++------ .../app/menus/MessageContextMenu.tsx | 21 +++++- .../ui/components/design/MenuButton.tsx | 5 +- .../picker/CompositionMediaPicker.tsx | 49 +++++++------ .../messaging/elements/Attachment.tsx | 22 ++++-- .../features/messaging/elements/Container.tsx | 13 +++- .../messaging/elements/MessageToolbar.tsx | 52 ++++++-------- .../features/messaging/elements/Reactions.tsx | 43 ++++-------- .../src/interface/channels/ChannelHeader.tsx | 13 ++-- .../interface/channels/text/TextChannel.tsx | 7 +- 10 files changed, 175 insertions(+), 118 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/Message.tsx b/packages/client/components/app/interface/channels/text/Message.tsx index 0471e0d949..e2cdb48dab 100644 --- a/packages/client/components/app/interface/channels/text/Message.tsx +++ b/packages/client/components/app/interface/channels/text/Message.tsx @@ -13,6 +13,7 @@ import { useState } from "@revolt/state"; import { Attachment, Avatar, + CompositionMediaPicker, Embed, MessageContainer, MessageReply, @@ -30,6 +31,8 @@ import { floatingUserMenusFromMessage, } from "../../../menus/UserContextMenu"; +import { startsWithPackPUA } from "@revolt/markdown/emoji/UnicodeEmoji"; +import { MediaPickerProps } from "@revolt/ui/components/features/messaging/composition/picker/CompositionMediaPicker"; import { EditMessage } from "./EditMessage"; /** @@ -75,6 +78,8 @@ export function Message(props: Props) { const client = useClient(); const [isHovering, setIsHovering] = createSignal(false); + const [reactPicker, setReactPicker] = createSignal(); + let msgRef; /** * Determine whether this message only contains a GIF @@ -104,6 +109,8 @@ export function Message(props: Props) { return ( + : () => ( + + ) } timestamp={props.message.createdAt} edited={props.message.editedAt} @@ -270,6 +282,29 @@ export function Message(props: Props) { } > + + props.message?.channel?.sendMessage({ + content, + replies: [{ id: props.message.id, mention: true }], + }) + } + onTextReplacement={(emoji) => + react( + emoji.startsWith(":") + ? emoji.slice(1, emoji.length - 1) + : startsWithPackPUA(emoji) + ? emoji.slice(1) + : emoji, + ) + } + > + {(trigProps) => { + trigProps.ref(msgRef); + setReactPicker(trigProps); + return <>; + }} + - - - {(attachment) => ( - - )} - - - - - {(embed) => } - - + + {(attachment) => ( + + )} + + + {(embed) => } + >} interactions={props.message.interactions} userId={client().user!.id} addReaction={react} removeReaction={unreact} - sendGIF={(content) => - props.message?.channel?.sendMessage({ - content, - replies: [{ id: props.message.id, mention: true }], - }) - } + reactPicker={reactPicker} /> ); diff --git a/packages/client/components/app/menus/MessageContextMenu.tsx b/packages/client/components/app/menus/MessageContextMenu.tsx index b75dbf39b0..c7dd679654 100644 --- a/packages/client/components/app/menus/MessageContextMenu.tsx +++ b/packages/client/components/app/menus/MessageContextMenu.tsx @@ -1,4 +1,4 @@ -import { For, Match, Show, Switch } from "solid-js"; +import { Accessor, For, Match, Show, Switch } from "solid-js"; import { Trans } from "@lingui-solid/solid/macro"; import { File, Message } from "stoat.js"; @@ -14,6 +14,7 @@ import MdDelete from "@material-design-icons/svg/outlined/delete.svg?component-s import MdDeleteSweep from "@material-design-icons/svg/outlined/delete_sweep.svg?component-solid"; import MdDownload from "@material-design-icons/svg/outlined/download.svg?component-solid"; import MdEdit from "@material-design-icons/svg/outlined/edit.svg?component-solid"; +import MdEmojiEmotions from "@material-design-icons/svg/outlined/emoji_emotions.svg?component-solid"; import MdLink from "@material-design-icons/svg/outlined/link.svg?component-solid"; import MdMarkChatUnread from "@material-design-icons/svg/outlined/mark_chat_unread.svg?component-solid"; import MdOpenInNew from "@material-design-icons/svg/outlined/open_in_new.svg?component-solid"; @@ -25,6 +26,7 @@ import MdShield from "@material-design-icons/svg/outlined/shield.svg?component-s import MdSentimentContent from "@material-symbols/svg-400/outlined/sentiment_content.svg?component-solid"; +import { MediaPickerProps } from "@revolt/ui/components/features/messaging/composition/picker/CompositionMediaPicker"; import { ContextMenu, ContextMenuButton, @@ -35,7 +37,11 @@ import { /** * Context menu for messages */ -export function MessageContextMenu(props: { message?: Message; file?: File }) { +export function MessageContextMenu(props: { + message?: Message; + file?: File; + reactPicker: Accessor; +}) { const user = useUser(); const state = useState(); const client = useClient(); @@ -162,7 +168,18 @@ export function MessageContextMenu(props: { message?: Message; file?: File }) { Copy text + + + + props.reactPicker()?.onClickEmoji(e)} + > + React + + + ; + +export type MediaPickerProps = { + ref: AnyRef; + onClickGif: (_: MouseEvent, ref?: AnyRef) => void; + onClickEmoji: (_: MouseEvent, ref?: AnyRef) => void; +}; + interface Props { /** * User card trigger area - * @param triggerProps Props that need to be applied to the trigger area + * @param trigProps Props that need to be applied to the trigger area */ - children: (triggerProps: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ref: Ref; - onClickGif: () => void; - onClickEmoji: () => void; - }) => JSX.Element; + children: (trigProps: MediaPickerProps) => JSX.Element; /** * Send a message @@ -55,19 +59,24 @@ export const CompositionMediaPickerContext = createContext( export function CompositionMediaPicker(props: Props) { const [anchor, setAnchor] = createSignal(); const [show, setShow] = createSignal<"gif" | "emoji">(); + let altRef: AnyRef | undefined; return ( {props.children({ ref: setAnchor, - onClickGif: () => - setShow((current) => (current === "gif" ? undefined : "gif")), - onClickEmoji: () => - setShow((current) => (current === "emoji" ? undefined : "emoji")), + onClickGif: (_, ref) => { + altRef = ref; + setShow((current) => (current === "gif" ? undefined : "gif")); + }, + onClickEmoji: (_, ref) => { + altRef = ref; + setShow((current) => (current === "emoji" ? undefined : "emoji")); + }, })} - - - + + + altRef || anchor()} show={show} setShow={setShow} onMessage={props.onMessage} onTextReplacement={props.onTextReplacement} /> - - - + + + ); } @@ -103,8 +112,8 @@ function Picker( middleware: [offset(5), flip(), shift()], }); - function onMouseDown() { - props.setShow(); + function onMouseDown(ev: MouseEvent) { + if (!floating()?.contains(ev.target as never)) props.setShow(); } onMount(() => document.addEventListener("mousedown", onMouseDown)); diff --git a/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx b/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx index bc0273d7b7..38b97983ee 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx @@ -1,4 +1,4 @@ -import { Match, Show, Switch } from "solid-js"; +import { Accessor, Match, Show, Switch } from "solid-js"; import { File, ImageEmbed, Message, VideoEmbed } from "stoat.js"; import { css } from "styled-system/css"; @@ -9,6 +9,7 @@ import { useModals } from "@revolt/modal"; import { Column } from "@revolt/ui/components/layout"; import { SizedContent, Spoiler } from "@revolt/ui/components/utils"; +import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; import { FileInfo } from "./FileInfo"; import { TextFile } from "./TextFile"; @@ -27,7 +28,11 @@ export const AttachmentContainer = styled(Column, { /** * Render a given list of files */ -export function Attachment(props: { file: File; message?: Message }) { +export function Attachment(props: { + file: File; + message?: Message; + reactPicker: Accessor; +}) { const { openModal } = useModals(); return ( @@ -52,7 +57,11 @@ export function Attachment(props: { file: File; message?: Message }) { src={props.file.createFileURL()} use:floating={{ contextMenu: () => ( - + ), }} /> @@ -72,7 +81,11 @@ export function Attachment(props: { file: File; message?: Message }) { src={props.file.originalUrl} use:floating={{ contextMenu: () => ( - + ), }} /> @@ -90,6 +103,7 @@ export function Attachment(props: { file: File; message?: Message }) { ), }} diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index e209963d76..ffb71d0430 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -1,4 +1,4 @@ -import { JSX, Match, Show, Switch } from "solid-js"; +import { Accessor, JSX, Match, Ref, Show, Switch } from "solid-js"; import { useLingui } from "@lingui-solid/solid/macro"; import { Message } from "stoat.js"; @@ -14,6 +14,7 @@ import { } from "@revolt/ui/components/utils"; import { useState } from "@revolt/state"; +import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; import { MessageToolbar } from "./MessageToolbar"; interface CommonProps { @@ -103,6 +104,10 @@ type Props = CommonProps & { */ contextMenu?: () => JSX.Element; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ref?: Ref; + reactPicker: Accessor; + /** * Additional match cases for the inline-start information element */ @@ -312,6 +317,7 @@ export function MessageContainer(props: Props) { return (
props.onHover && props.onHover(true)} onMouseLeave={() => props.onHover && props.onHover(false)} class={ @@ -334,7 +340,10 @@ export function MessageContainer(props: Props) { props.isLink !== "hide" } > - + diff --git a/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx b/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx index 067aede7d4..e886460618 100644 --- a/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx @@ -1,4 +1,4 @@ -import { Show } from "solid-js"; +import { Accessor, Show } from "solid-js"; import { Message } from "stoat.js"; import { cva } from "styled-system/css"; @@ -17,13 +17,16 @@ import MdEmojiEmotions from "@material-design-icons/svg/outlined/emoji_emotions. import MdMoreVert from "@material-design-icons/svg/outlined/more_vert.svg?component-solid"; import MdReply from "@material-design-icons/svg/outlined/reply.svg?component-solid"; -import { startsWithPackPUA } from "@revolt/markdown/emoji/UnicodeEmoji"; -import { CompositionMediaPicker } from "../composition"; +import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; -export function MessageToolbar(props: { message?: Message }) { +export function MessageToolbar(props: { + message?: Message; + reactPicker: Accessor; +}) { const user = useUser(); const state = useState(); const { openModal } = useModals(); + let reactRef; // todo: a11y for buttons; tabindex @@ -53,34 +56,14 @@ export function MessageToolbar(props: { message?: Message }) {
- - props.message?.channel?.sendMessage({ - content, - replies: [{ id: props.message.id, mention: true }], - }) - } - onTextReplacement={(emoji) => - props.message!.react( - emoji.startsWith(":") - ? emoji.slice(1, emoji.length - 1) - : startsWithPackPUA(emoji) - ? emoji.slice(1) - : emoji, - ) - } +
props.reactPicker()?.onClickEmoji(e, reactRef)} > - {(triggerProps) => ( -
- - -
- )} - + + +
, + contextMenu: () => ( + + ), contextMenuHandler: "click", }} > diff --git a/packages/client/components/ui/components/features/messaging/elements/Reactions.tsx b/packages/client/components/ui/components/features/messaging/elements/Reactions.tsx index 37cb595e07..ce08915725 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Reactions.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Reactions.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo } from "solid-js"; +import { Accessor, For, Show, createMemo } from "solid-js"; import { useLingui } from "@lingui-solid/solid/macro"; import { API } from "stoat.js"; @@ -12,8 +12,7 @@ import { Row } from "@revolt/ui/components/layout"; import MdAdd from "@material-design-icons/svg/outlined/add.svg?component-solid"; -import { startsWithPackPUA } from "@revolt/markdown/emoji/UnicodeEmoji"; -import { CompositionMediaPicker } from "../composition"; +import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; interface Props { /** @@ -37,17 +36,13 @@ interface Props { */ addReaction(reaction: string): void; - /** - * Send a GIF reaction - * @param text Message - */ - sendGIF(text: string): void; - /** * Remove a reaction * @param reaction ID */ removeReaction(reaction: string): void; + + reactPicker: Accessor; } /** @@ -93,6 +88,8 @@ export function Reactions(props: Props) { : required.length || optional.length; }; + let reactRef; + return ( @@ -121,27 +118,15 @@ export function Reactions(props: Props) { /> )} - - props.addReaction( - emoji.startsWith(":") - ? emoji.slice(1, emoji.length - 1) - : startsWithPackPUA(emoji) - ? emoji.slice(1) - : emoji, - ) - } +
props.reactPicker()?.onClickEmoji(e, reactRef)} > - {(triggerProps) => ( -
- - - - -
- )} - + + + + +
); diff --git a/packages/client/src/interface/channels/ChannelHeader.tsx b/packages/client/src/interface/channels/ChannelHeader.tsx index d08c3c2aa2..280967a424 100644 --- a/packages/client/src/interface/channels/ChannelHeader.tsx +++ b/packages/client/src/interface/channels/ChannelHeader.tsx @@ -17,8 +17,8 @@ import { NonBreakingText, OverflowingText, Spacer, - UserStatus, typography, + UserStatus, } from "@revolt/ui"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; @@ -29,7 +29,7 @@ import MdSettings from "@material-design-icons/svg/outlined/settings.svg?compone import MdKeep from "../../svg/keep.svg?component-solid"; import { HeaderIcon } from "../common/CommonHeader"; -import { SidebarState } from "./text/TextChannel"; +import { canIHasSidebar, SidebarState } from "./text/TextChannel"; interface Props { /** @@ -62,11 +62,8 @@ export function ChannelHeader(props: Props) { if (!props.sidebarState) return null; const state = props.sidebarState(); - if (state.state === "search") { - return state.query; - } else { - return ""; - } + if (state.state === "search") return state.query; + return ""; }; return ( @@ -219,7 +216,7 @@ export function ChannelHeader(props: Props) { - + { if (props.sidebarState!().state === "default") { diff --git a/packages/client/src/interface/channels/text/TextChannel.tsx b/packages/client/src/interface/channels/text/TextChannel.tsx index c4a539c19f..e79591a0c2 100644 --- a/packages/client/src/interface/channels/text/TextChannel.tsx +++ b/packages/client/src/interface/channels/text/TextChannel.tsx @@ -31,6 +31,7 @@ import { VoiceChannelCallCardMount } from "@revolt/ui/components/features/voice/ import { ChannelHeader } from "../ChannelHeader"; import { ChannelPageProps } from "../ChannelPage"; +import { Channel } from "stoat.js"; import { MessageComposition } from "./Composition"; import { MemberSidebar } from "./MemberSidebar"; import { TextSearchSidebar } from "./TextSearchSidebar"; @@ -50,6 +51,10 @@ export type SidebarState = state: "default"; }; +export function canIHasSidebar(ch: Channel) { + return !["SavedMessages", "DirectMessage"].includes(ch.type); +} + /** * Channel component */ @@ -217,7 +222,7 @@ export function TextChannel(props: ChannelPageProps) { LAYOUT_SECTIONS.MEMBER_SIDEBAR, true, ) && - props.channel.type !== "SavedMessages") || + canIHasSidebar(props.channel)) || sidebarState().state !== "default" } > From 357778024093a570ed41be035a593ff25ca7e0f6 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:16:37 -0500 Subject: [PATCH 18/34] fix: Prettier format Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/features/texteditor/codeMirrorLineWrap.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts b/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts index d8e9fdae4f..a6b86b894c 100644 --- a/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts +++ b/packages/client/components/ui/components/features/texteditor/codeMirrorLineWrap.ts @@ -168,14 +168,14 @@ const lineWrapStyles = EditorView.theme({ textIndent: 0, }, ".cm-content": { - minWidth: 0 + minWidth: 0, }, ".cm-placeholder": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", - width: "100%" - } + width: "100%", + }, }); export const smartLineWrapping = [ From 8cdc2b0bd81a866b153a40f313901eca9036779a Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 9 May 2026 15:03:38 -0400 Subject: [PATCH 19/34] fix: Reset floating touch timer to prevent overlapping timer bug - Fix missing reactPicker in AppearanceMenu Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../settings/user/appearance/AppearanceMenu.tsx | 2 ++ packages/client/components/ui/directives/floating.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx b/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx index f29fd01140..e6673e5819 100644 --- a/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx +++ b/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx @@ -295,6 +295,7 @@ export function AppearanceMenu() { undefined} avatar={ undefined} avatar={} timestamp={new Date()} username={"MysticPixie"} diff --git a/packages/client/components/ui/directives/floating.ts b/packages/client/components/ui/directives/floating.ts index 3171623d8c..8b3cd42df6 100644 --- a/packages/client/components/ui/directives/floating.ts +++ b/packages/client/components/ui/directives/floating.ts @@ -125,7 +125,8 @@ export function floating(element: HTMLElement, accessor: Accessor) { trigger("contextMenu"); } - let isTouch = false; + let isTouch = false, + tTmr: NodeJS.Timeout | undefined; /** * Handle mouse entering @@ -143,7 +144,11 @@ export function floating(element: HTMLElement, accessor: Accessor) { function onTouch() { isTouch = true; - setTimeout(() => (isTouch = false), 100); + clearTimeout(tTmr); + tTmr = setTimeout(() => { + isTouch = false; + tTmr = undefined; + }, 100); } createEffect( From 11ae2944b46dda22fd151c9f4adb0b411dbae69f Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 8 May 2026 04:42:23 -0400 Subject: [PATCH 20/34] fix: Misc emoji and message box fixes - Don't autofocus search input in GIF/emoji picker on mobile because it causes unwanted touch keyboard deployment - Shrink GIF/emoji picker when screen is too small (dynamically reduces row count for emoji picker or GIF size for GIF picker) - Prevent GIF/emoji picker from overflowing screen width/height (approach for this is a bit messy, could be improved) - Fix different sized margin on left vs right side of message box (if you had OCD you'd understand) - Fix overflow hidden clipping emoji autocorrect dropdown when editing a message - Fix message box buttons/send button not firing when touch keyboard is open on Chrome (testing needed on Firefox/Safari mobile) - Fix vite config not allowing rotate to landscape Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../interface/channels/text/DraftMessage.tsx | 1 + .../messaging/composition/MessageBox.tsx | 24 +++++------ .../picker/CompositionMediaPicker.tsx | 40 ++++++++++++++----- .../composition/picker/EmojiPicker.tsx | 32 +++++++-------- .../composition/picker/GifPicker.tsx | 28 ++++++------- .../features/messaging/elements/Container.tsx | 1 + .../ui/components/navigation/SlideDrawer.ts | 2 +- .../interface/channels/text/Composition.tsx | 25 ++++++------ packages/client/vite.config.ts | 2 +- 9 files changed, 84 insertions(+), 71 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/DraftMessage.tsx b/packages/client/components/app/interface/channels/text/DraftMessage.tsx index 0eadf3b484..9ffb910e1f 100644 --- a/packages/client/components/app/interface/channels/text/DraftMessage.tsx +++ b/packages/client/components/app/interface/channels/text/DraftMessage.tsx @@ -66,6 +66,7 @@ export function DraftMessage(props: Props) { )} compact={state.settings.getValue("appearance:compact_mode")} + reactPicker={() => undefined} > diff --git a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx index ca8ffd58c6..faa6df19bb 100644 --- a/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/MessageBox.tsx @@ -97,8 +97,7 @@ const Base = styled("div", { flexGrow: 1, minWidth: 0, - paddingInlineEnd: "var(--gap-md)", - paddingBlock: "var(--gap-sm)", + padding: "var(--gap-sm) var(--gap-md)", borderStartRadius: "var(--borderRadius-xl)", display: "flex", @@ -142,6 +141,9 @@ const Blocked = styled(Row, { userSelect: "none", padding: "var(--gap-md)", }, + variants: { + noPad: { true: { padding: 0 } }, + }, }); /** @@ -156,17 +158,13 @@ export const InlineIcon = styled("div", { }, variants: { size: { - short: { - width: "14px", - }, - normal: { - width: "42px", - }, - wide: { - width: "62px", - }, + short: { width: "14px" }, + normal: { width: "42px" }, }, }, + defaultVariants: { + size: "normal", + }, }); const FloatingAction = styled("div", { @@ -233,7 +231,7 @@ export function MessageBox(props: Props) { - + @@ -258,7 +256,7 @@ export function MessageBox(props: Props) { } > - + You don't have permission to send messages in this channel. diff --git a/packages/client/components/ui/components/features/messaging/composition/picker/CompositionMediaPicker.tsx b/packages/client/components/ui/components/features/messaging/composition/picker/CompositionMediaPicker.tsx index cca01bdbd6..59d73a4586 100644 --- a/packages/client/components/ui/components/features/messaging/composition/picker/CompositionMediaPicker.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/picker/CompositionMediaPicker.tsx @@ -30,8 +30,8 @@ type AnyRef = Ref; export type MediaPickerProps = { ref: AnyRef; - onClickGif: (_: MouseEvent, ref?: AnyRef) => void; - onClickEmoji: (_: MouseEvent, ref?: AnyRef) => void; + onClickGif: (_: AnyRef, ref?: AnyRef) => void; + onClickEmoji: (_: AnyRef, ref?: AnyRef) => void; }; interface Props { @@ -106,6 +106,7 @@ function Picker( }, ) { const [floating, setFloating] = createSignal(); + const [fixed, setFixed] = createSignal(false); const position = useFloating(() => props.anchor(), floating, { placement: "top-end", @@ -115,18 +116,36 @@ function Picker( function onMouseDown(ev: MouseEvent) { if (!floating()?.contains(ev.target as never)) props.setShow(); } + function onResize() { + const el = floating(); + if (!el) return; + const rect = el.getBoundingClientRect(); - onMount(() => document.addEventListener("mousedown", onMouseDown)); - onCleanup(() => document.removeEventListener("mousedown", onMouseDown)); + //Prevent overflow off-screen + if (rect.right > innerWidth || rect.bottom > innerHeight) setFixed(true); + } + onMount(() => { + addEventListener("mousedown", onMouseDown); + addEventListener("resize", onResize); + setTimeout(onResize, 1); + }); + onCleanup(() => { + removeEventListener("mousedown", onMouseDown); + removeEventListener("resize", onResize); + }); return ( @@ -166,7 +185,8 @@ const Base = styled("div", { base: { width: "400px", height: "400px", - // paddingInlineEnd: "5px", + maxWidth: "100%", + maxHeight: "calc(100% - 72px)", }, }); diff --git a/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx b/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx index 1e6cf40cca..79d05ac012 100644 --- a/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx @@ -4,6 +4,7 @@ import { Switch, createMemo, createSignal, + onMount, useContext, } from "solid-js"; @@ -63,17 +64,20 @@ type Item = text: string; }; -const COLUMNS = 10; - export function EmojiPicker() { const client = useClient(); const state = useState(); const [filter, setFilter] = createSignal(""); + const [colCount, setColCount] = createSignal(0); let serverScrollTargetElement!: HTMLDivElement; let emojiScrollTargetElement!: HTMLDivElement; + onMount(() => + setColCount(Math.floor(emojiScrollTargetElement.offsetWidth / 40)), + ); + const items = createMemo(() => { const filterText = filter().toLowerCase(); @@ -92,7 +96,8 @@ export function EmojiPicker() { ] as Item[]; } - const items: Item[] = []; + const items: Item[] = [], + cols = colCount(); for (const server of state.ordering.orderedServers(client())) { const emojis = server.emojis; @@ -104,7 +109,7 @@ export function EmojiPicker() { server, }); - while (items.length % COLUMNS) { + while (items.length % cols) { items.push({ t: 1 }); } @@ -112,7 +117,7 @@ export function EmojiPicker() { items.push({ t: 2, emoji }); } - while (items.length % COLUMNS) { + while (items.length % cols) { items.push({ t: 1 }); } } @@ -122,7 +127,7 @@ export function EmojiPicker() { title: "Default", }); - while (items.length % COLUMNS) { + while (items.length % cols) { items.push({ t: 1 }); } @@ -140,15 +145,10 @@ export function EmojiPicker() { return ( { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - }} onInput={(e) => setFilter(e.currentTarget.value)} /> @@ -176,11 +176,7 @@ export function EmojiPicker() { items={items()} scrollTarget={emojiScrollTargetElement} itemSize={{ height: 40, width: 40 }} - crossAxisCount={(measurements) => - Math.floor( - measurements.container.cross / measurements.itemSize.cross, - ) - } + crossAxisCount={colCount} > {EmojiItem} @@ -301,7 +297,7 @@ const EmojiOption = styled("div", { display: "flex", alignItems: "center", paddingInline: "var(--gap-md)", - width: `calc(40px * ${COLUMNS}) !important`, + width: "100% !important", }, }, { diff --git a/packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx b/packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx index 5c79cc1a77..1a5f8b1653 100644 --- a/packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx @@ -21,6 +21,7 @@ import { typography, } from "@revolt/ui/components/design"; +import { useState } from "@revolt/state"; import { CompositionMediaPickerContext } from "./CompositionMediaPicker"; type GifCategory = { title: string; image: string }; @@ -33,22 +34,17 @@ type GifResult = { const FilterContext = createContext<(value: string) => void>(); export function GifPicker() { + const { isMobile } = useState(); const [filter, setFilter] = createSignal(""); - const fliterLowercase = () => filter().toLowerCase(); return ( { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - }} onChange={(e) => setFilter(e.currentTarget.value)} /> }> @@ -146,10 +142,11 @@ function Categories() { - Math.floor(measurements.container.cross / measurements.itemSize.cross) - } + itemSize={(contWidth) => ({ + width: contWidth / 2, + height: Math.floor((contWidth * 3) / 10), + })} + crossAxisCount={() => 2} > {CategoryItem} @@ -243,10 +240,11 @@ function GifSearch(props: { query: string }) { - Math.floor(measurements.container.cross / measurements.itemSize.cross) - } + itemSize={(contWidth) => ({ + width: contWidth / 2, + height: Math.floor((contWidth * 3) / 10), + })} + crossAxisCount={() => 2} > {GifItem} diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index ffb71d0430..10538874e1 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -242,6 +242,7 @@ const Body = styled("div", { editing: { true: { flexGrow: 1, + overflow: "visible", }, }, }, diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index 40b4439c57..00a3d9b00d 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -1,4 +1,4 @@ -const ANIM_MS = 200, +const ANIM_MS = 150, VEL_MS = 33, //30Hz velocity update VEL_AVG = 5, //Moving avg smoothing VEL_TRIG = 0.3, //Override drawer pos if over diff --git a/packages/client/src/interface/channels/text/Composition.tsx b/packages/client/src/interface/channels/text/Composition.tsx index 0bbd088acf..590dd40acc 100644 --- a/packages/client/src/interface/channels/text/Composition.tsx +++ b/packages/client/src/interface/channels/text/Composition.tsx @@ -1,9 +1,7 @@ import { createCountdownFromNow } from "@solid-primitives/date"; import { For, - Match, Show, - Switch, createEffect, createMemo, createSignal, @@ -446,15 +444,16 @@ export function MessageComposition(props: Props) { content={draft()?.content ?? ""} setContent={setContent} actionsStart={ - }> - - - - add - - - - + } + > + + + add + + + } actionsEnd={ @@ -476,13 +475,13 @@ export function MessageComposition(props: Props) { {(triggerProps) => ( <> - + gif - + emoticon diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 2e502c1b3a..d868fd8b69 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -42,7 +42,7 @@ export default defineConfig({ description: "User-first open source chat platform.", categories: ["communication", "chat", "messaging"], start_url: base, - orientation: "portrait", + orientation: "any", display_override: ["window-controls-overlay"], display: "standalone", background_color: "#101823", From 9d1997398be189382f162aada3a8b5e1521ddbab Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Mar 2026 02:42:04 -0400 Subject: [PATCH 21/34] fix: Misc console warnings - Fix 'scrollTo undefined' error in Message - Fix 'computations will never be disposed' warning in Container - Add excluded generated dirs from eslint config to vscode config Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../app/interface/channels/text/Messages.tsx | 4 +- .../features/messaging/elements/Container.tsx | 71 ++++++++++--------- .../features/messaging/elements/TextEmbed.tsx | 6 +- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/Messages.tsx b/packages/client/components/app/interface/channels/text/Messages.tsx index d045e4e4d3..21883d75b0 100644 --- a/packages/client/components/app/interface/channels/text/Messages.tsx +++ b/packages/client/components/app/interface/channels/text/Messages.tsx @@ -281,7 +281,7 @@ export function Messages(props: Props) { // If we're not at the end, restore scroll position if (existingState && !existingState.atEnd) { setTimeout(() => - listRef!.scrollTo({ + listRef?.scrollTo({ top: existingState.scrollTop!, behavior: "instant", }), @@ -290,7 +290,7 @@ export function Messages(props: Props) { // Or... reset scroll to the end else if (atEnd()) { setTimeout(() => - listRef!.scrollTo({ + listRef?.scrollTo({ top: 9999999, behavior: "instant", }), diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index 10538874e1..a79af94bc4 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -363,7 +363,7 @@ export function MessageContainer(props: Props) { use:floating={{ tooltip: { placement: "top", - content: ( + content: () => ( <> {t`Sent`}{" "} - ) as string, // ignore aria requirement + ), + aria: "", }, }} > @@ -436,39 +438,41 @@ export function MessageContainer(props: Props) {
{props.info} - - - - {t`Sent`}{" "} - - - + + ( + <> + {t`Sent`}{" "} + + ( <> {t`Edited`}{" "} From 2123301d1ae26331a31053a602946e0e9d23bd50 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:56:40 -0500 Subject: [PATCH 22/34] fix: Make reactPicker optional to make method signatures backward compatible Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../components/app/interface/channels/text/DraftMessage.tsx | 1 - .../interface/settings/user/appearance/AppearanceMenu.tsx | 2 -- packages/client/components/app/menus/MessageContextMenu.tsx | 4 ++-- .../ui/components/features/messaging/elements/Attachment.tsx | 2 +- .../ui/components/features/messaging/elements/Container.tsx | 2 +- .../features/messaging/elements/MessageToolbar.tsx | 4 ++-- .../ui/components/features/messaging/elements/TextEmbed.tsx | 5 +---- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/client/components/app/interface/channels/text/DraftMessage.tsx b/packages/client/components/app/interface/channels/text/DraftMessage.tsx index 9ffb910e1f..0eadf3b484 100644 --- a/packages/client/components/app/interface/channels/text/DraftMessage.tsx +++ b/packages/client/components/app/interface/channels/text/DraftMessage.tsx @@ -66,7 +66,6 @@ export function DraftMessage(props: Props) { )} compact={state.settings.getValue("appearance:compact_mode")} - reactPicker={() => undefined} > diff --git a/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx b/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx index e6673e5819..f29fd01140 100644 --- a/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx +++ b/packages/client/components/app/interface/settings/user/appearance/AppearanceMenu.tsx @@ -295,7 +295,6 @@ export function AppearanceMenu() { undefined} avatar={ undefined} avatar={} timestamp={new Date()} username={"MysticPixie"} diff --git a/packages/client/components/app/menus/MessageContextMenu.tsx b/packages/client/components/app/menus/MessageContextMenu.tsx index c7dd679654..6d367bdeb4 100644 --- a/packages/client/components/app/menus/MessageContextMenu.tsx +++ b/packages/client/components/app/menus/MessageContextMenu.tsx @@ -40,7 +40,7 @@ import { export function MessageContextMenu(props: { message?: Message; file?: File; - reactPicker: Accessor; + reactPicker?: Accessor; }) { const user = useUser(); const state = useState(); @@ -174,7 +174,7 @@ export function MessageContextMenu(props: { props.reactPicker()?.onClickEmoji(e)} + onClick={(e) => props.reactPicker?.()?.onClickEmoji(e)} > React diff --git a/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx b/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx index 38b97983ee..c46b4091ee 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Attachment.tsx @@ -31,7 +31,7 @@ export const AttachmentContainer = styled(Column, { export function Attachment(props: { file: File; message?: Message; - reactPicker: Accessor; + reactPicker?: Accessor; }) { const { openModal } = useModals(); diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index a79af94bc4..d522a85936 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -106,7 +106,7 @@ type Props = CommonProps & { // eslint-disable-next-line @typescript-eslint/no-explicit-any ref?: Ref; - reactPicker: Accessor; + reactPicker?: Accessor; /** * Additional match cases for the inline-start information element diff --git a/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx b/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx index e886460618..27aa1e50b2 100644 --- a/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/MessageToolbar.tsx @@ -21,7 +21,7 @@ import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; export function MessageToolbar(props: { message?: Message; - reactPicker: Accessor; + reactPicker?: Accessor; }) { const user = useUser(); const state = useState(); @@ -59,7 +59,7 @@ export function MessageToolbar(props: {
props.reactPicker()?.onClickEmoji(e, reactRef)} + onClick={(e) => props.reactPicker?.()?.onClickEmoji(e, reactRef)} > diff --git a/packages/client/components/ui/components/features/messaging/elements/TextEmbed.tsx b/packages/client/components/ui/components/features/messaging/elements/TextEmbed.tsx index efdd2be82d..929bee7363 100644 --- a/packages/client/components/ui/components/features/messaging/elements/TextEmbed.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/TextEmbed.tsx @@ -134,10 +134,7 @@ export function TextEmbed(props: { embed: TextEmbedClass | WebsiteEmbed }) { props.embed.type === "Text" && (props.embed as TextEmbedClass).media } > - undefined} - /> + From e92b5d4fa72134681f728fcecdc00c3166685b76 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:32:44 -0500 Subject: [PATCH 23/34] fix: Use text cursor on mdui-text-field Addresses #1005 Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../client/components/ui/components/design/TextField.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/client/components/ui/components/design/TextField.tsx b/packages/client/components/ui/components/design/TextField.tsx index 5c6f7adb99..8cfb49713b 100644 --- a/packages/client/components/ui/components/design/TextField.tsx +++ b/packages/client/components/ui/components/design/TextField.tsx @@ -2,6 +2,7 @@ import type { JSX } from "solid-js"; import "mdui/components/select.js"; import "mdui/components/text-field.js"; +import { cva } from "styled-system/css"; type Props = JSX.HTMLAttributes & { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -78,6 +79,10 @@ type Props = JSX.HTMLAttributes & { tabindex?: number; }; +const field = cva({ + base: { cursor: "text" }, +}); + /** * Text fields let users enter text into a UI * @@ -88,6 +93,7 @@ export function TextField(props: Props) { return ( ); From 498b9ae84d75f346d1fc439b03b435c3e96cfefe Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:13:46 -0400 Subject: [PATCH 24/34] fix: Make class naming scheme compatible with #791 Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../interface/settings/_layout/Content.tsx | 6 ++-- .../interface/settings/_layout/Sidebar.tsx | 4 +-- .../settings/_layout/SidebarButton.tsx | 18 ++++++++--- .../interface/settings/user/_AccountCard.tsx | 2 +- .../components/modal/modals/Settings.tsx | 2 +- .../ui/components/design/Dialog.tsx | 3 +- packages/client/components/ui/styles.css | 32 +++++++++---------- packages/client/src/Interface.tsx | 4 +-- packages/client/src/interface/Sidebar.tsx | 2 +- .../navigation/channels/HomeSidebar.tsx | 2 +- .../navigation/channels/ServerSidebar.tsx | 2 +- 11 files changed, 43 insertions(+), 34 deletions(-) diff --git a/packages/client/components/app/interface/settings/_layout/Content.tsx b/packages/client/components/app/interface/settings/_layout/Content.tsx index f9f1c4a97a..5760c67591 100644 --- a/packages/client/components/app/interface/settings/_layout/Content.tsx +++ b/packages/client/components/app/interface/settings/_layout/Content.tsx @@ -24,9 +24,9 @@ export function SettingsContent(props: { const { navigate } = useSettingsNavigation(); return ( -
+
- + @@ -45,7 +45,7 @@ export function SettingsContent(props: { - + diff --git a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx index 1635b0a730..e300cff814 100644 --- a/packages/client/components/app/interface/settings/_layout/Sidebar.tsx +++ b/packages/client/components/app/interface/settings/_layout/Sidebar.tsx @@ -36,9 +36,9 @@ export function SettingsSidebar(props: { }); return ( - +
- + {list.prepend} diff --git a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx index 8cac2b5523..7e57d72f30 100644 --- a/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx +++ b/packages/client/components/app/interface/settings/_layout/SidebarButton.tsx @@ -6,19 +6,27 @@ import { styled } from "styled-system/jsx"; * Sidebar button */ export function SidebarButton( - props: JSX.HTMLAttributes & { noDrawer?: boolean }, + props: JSX.HTMLAttributes & { + "aria-selected"?: boolean; + noDrawer?: boolean; + }, ) { const { diagDrawer } = useState(); - const [local, other] = splitProps(props, ["onClick"]); + const [local, other] = splitProps(props, ["onClick", "noDrawer", "class"]); function onClick(e: Event) { - if (!props.noDrawer) diagDrawer()?.setShown(true); + if (!local.noDrawer) diagDrawer()?.setShown(true); // @ts-expect-error callable listener if (local.onClick) local.onClick(e); } - // @ts-expect-error todo dunno about this error - return ; + return ( + + ); } const SidebarButtonBase = styled("a", { diff --git a/packages/client/components/app/interface/settings/user/_AccountCard.tsx b/packages/client/components/app/interface/settings/user/_AccountCard.tsx index e3fac36757..3c38a508db 100644 --- a/packages/client/components/app/interface/settings/user/_AccountCard.tsx +++ b/packages/client/components/app/interface/settings/user/_AccountCard.tsx @@ -45,7 +45,7 @@ export function AccountCard() { export function BackCard(props: { onClose?: () => void }) { return ( - + diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index 39f9b91e0e..dd6c077721 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -51,7 +51,7 @@ export function SettingsModal( * { width: 100% } - .setSidebar { max-width: unset } - .setBase { border-radius: 0 } - .setCont { height: 100vh } + .settings_sidebar > * { width: 100% } + .settings_sidebar .content { max-width: unset } + .settings { border-radius: 0 } + .settings_cont { height: 100vh } } /* HighlightJs */ diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index 22dc07f824..ad1266d541 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -88,7 +88,7 @@ const Interface = (props: { children: JSX.Element }) => { return ( -
+
}> @@ -120,7 +120,7 @@ const Interface = (props: { children: JSX.Element }) => { /> +
{ }); return ( - +
diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index 766ded06b4..209c6c4150 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -191,7 +191,7 @@ export const ServerSidebar = (props: Props) => { return ( Date: Wed, 18 Mar 2026 21:39:09 -0400 Subject: [PATCH 25/34] fix: Appearance menu and role editor color pane clipping past edge on mobile Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../settings/server/roles/ServerRoleEditor.tsx | 7 ++++--- .../settings/user/appearance/AppearanceMenu.tsx | 2 +- .../client/components/ui/components/design/Button.tsx | 10 +++++----- .../components/ui/components/design/IconButton.tsx | 3 ++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/client/components/app/interface/settings/server/roles/ServerRoleEditor.tsx b/packages/client/components/app/interface/settings/server/roles/ServerRoleEditor.tsx index 5d317f7ea8..7e5fa909d7 100644 --- a/packages/client/components/app/interface/settings/server/roles/ServerRoleEditor.tsx +++ b/packages/client/components/app/interface/settings/server/roles/ServerRoleEditor.tsx @@ -103,12 +103,13 @@ export function ServerRoleEditor(props: { context: Server; roleId: string }) { label={t`Role Name`} /> - + pickerRef()?.click()} > @@ -132,7 +133,7 @@ export function ServerRoleEditor(props: { context: Server; roleId: string }) { }} /> - + - + */} - + , - "role" | "tabIndex" | "aria-selected" + "role" | "tabIndex" | "aria-selected" | "style" >, "onClick" | "disabled" >; @@ -28,6 +28,7 @@ export function IconButton(props: Props) { "aria-selected", "tabIndex", "role", + "style", ]); const [style, rest] = splitProps(propsRest, [ From d1fdcef22f5ba78febd52dc20f3b9cc83dd23742 Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:20:08 -0400 Subject: [PATCH 26/34] fix: Adjust channel bar bottom margin to match top margin Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../client/src/interface/navigation/channels/ServerSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index 209c6c4150..f683bdb215 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -225,7 +225,7 @@ export const ServerSidebar = (props: Props) => {
Date: Mon, 30 Mar 2026 15:18:55 -0400 Subject: [PATCH 27/34] feat: Add reactive signals to SlideDrawer, will come in handy in other PRs Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../ui/components/navigation/SlideDrawer.ts | 61 +++++++++++++------ packages/client/src/Interface.tsx | 25 ++++---- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index 00a3d9b00d..8d989633fc 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -1,3 +1,5 @@ +import { createSignal } from "solid-js"; + const ANIM_MS = 150, VEL_MS = 33, //30Hz velocity update VEL_AVG = 5, //Moving avg smoothing @@ -18,18 +20,29 @@ type TrackTouch = { vOfs?: number; }; +export enum SlideState { + HIDDEN, + SHOWN, + HIDING, + SHOWING, + MOVING, +} + export class SlideDrawer { - enabled = false; private media; private touch: TrackTouch | null = null; private tTmr: NodeJS.Timeout | null = null; private vTmr: NodeJS.Timeout | null = null; private ofs = 0; + private eGet; + private eSet; + private sGet; + private sSet; + constructor( private drawer: HTMLElement, private root: HTMLElement, - public onStateChanged: ((en: boolean) => void) | null = null, ) { this.start = this.start.bind(this); this.move = this.move.bind(this); @@ -37,19 +50,27 @@ export class SlideDrawer { root.addEventListener("touchmove", this.move); root.addEventListener("touchend", this.move); + //Signals + const [eg, es] = createSignal(false); + this.eGet = eg; + this.eSet = es; + const [sg, ss] = createSignal(SlideState.HIDDEN); + this.sGet = sg; + this.sSet = ss; + //Auto-enable based on device width const pwMax = getComputedStyle(document.body).getPropertyValue( "--phone-max-width", ); - this.media = matchMedia(`(max-width: ${pwMax}`); - this.media.onchange = (e) => this.setEnabled(e.matches); - this.setEnabled(this.media.matches); + this.media = matchMedia(`(max-width: ${pwMax})`); + this.media.onchange = (e) => (this.enabled = e.matches); + this.enabled = this.media.matches; } private start(e: TouchEvent) { //Cancel if more than one finger if (e.touches.length > 1) return this.endTouch(); - if (this.touch || !this.enabled) return; + if (this.touch || !this.eGet()) return; //Track this touch const t = e.touches[0]; @@ -90,10 +111,11 @@ export class SlideDrawer { t.trig = trig = true; this.tfTimer(); this.velTimer(); + if (!isEnd) this.sSet(SlideState.MOVING); } dx = Math.max(Math.min(this.ofs + dx, 0), max); - ds.transform = `translateX(${dx}px)`; + if (!isEnd) ds.transform = `translateX(${dx}px)`; e.preventDefault(); e.stopPropagation(); } @@ -160,6 +182,7 @@ export class SlideDrawer { this.ofs = show ? -innerWidth : 0; ds.transition = `transform ${ANIM_MS}ms`; ds.transform = `translateX(${this.ofs}px)`; + this.sSet(show ? SlideState.SHOWING : SlideState.HIDING); } else { ds.transition = ds.transform = ""; } @@ -171,6 +194,7 @@ export class SlideDrawer { ds.transition = ds.transform = ""; this.setElState(show); this.tTmr = null; + this.sSet(show ? SlideState.SHOWN : SlideState.HIDDEN); }, ANIM_MS + 50) : null; } @@ -182,35 +206,38 @@ export class SlideDrawer { } delete() { - this.setEnabled(false); + this.enabled = false; this.root.removeEventListener("touchstart", this.start); this.root.removeEventListener("touchmove", this.move); this.root.removeEventListener("touchend", this.move); this.media.onchange = null; } - setEnabled(en: boolean) { - if (this.enabled !== en) { + get enabled() { + return this.eGet(); + } + set enabled(en: boolean) { + if (this.eGet() !== en) { this.drawer.style.zIndex = en ? "1" : ""; this.tfTimer(); this.endTouch(); if (!en) this.setElState(true); this.ofs = 0; + this.eSet(en); + this.sSet(SlideState.HIDDEN); } - this.enabled = en; - if (this.onStateChanged) this.onStateChanged(en); } - isShown() { - return this.ofs !== 0; + get state() { + return this.sGet(); } setShown(show: boolean) { - if (!this.enabled || this.touch?.trig || this.tTmr) return false; - if (this.isShown() !== show) { + if (!this.eGet() || this.touch?.trig || this.tTmr) return false; + if ((this.ofs !== 0) !== show) { this.setElState(false); this.drawer.style.transform = `translateX(${this.ofs}px)`; - setTimeout(() => this.tfTimer(true, show), 0); + setTimeout(() => this.tfTimer(true, show), 1); } return true; } diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index ad1266d541..e53514c748 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -3,7 +3,6 @@ import { createSignal, JSX, Match, - on, onCleanup, Switch, } from "solid-js"; @@ -70,17 +69,19 @@ const Interface = (props: { children: JSX.Element }) => { state.layout.setSectionState(LAYOUT_SECTIONS.PRIMARY_SIDEBAR, false, false); state.layout.setSectionState(LAYOUT_SECTIONS.MEMBER_SIDEBAR, false, true); } - createEffect( - on(contRef, (cont) => { - if (!cont || sDrawer) return; - sDrawer = new SlideDrawer(cont, rootRef!, (en) => { - setTimeout(() => { - state.setAppDrawer(en ? sDrawer : null); - if (en) rstLayout(); - }, 1); - }); - }), - ); + createEffect(() => { + //Create drawer + const cont = contRef(); + if (cont && !sDrawer) sDrawer = new SlideDrawer(cont, rootRef!); + //Update on layout change + if (sDrawer) { + const en = sDrawer.enabled; + setTimeout(() => { + state.setAppDrawer(en ? sDrawer : null); + if (en) rstLayout(); + }, 1); + } + }); onCleanup(() => { sDrawer?.delete(); state.setAppDrawer((sDrawer = null)); From bdfa8e0d688030543076073c094cfd9788d54c1f Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:47:41 -0400 Subject: [PATCH 28/34] fix: Use undefined rather than null for consistency - Start SlideState enum at 1 so that ! can be used to check for undefined Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/modal/modals/Settings.tsx | 4 ++-- packages/client/components/state/index.tsx | 4 ++-- .../components/ui/components/navigation/SlideDrawer.ts | 2 +- packages/client/src/Interface.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/client/components/modal/modals/Settings.tsx b/packages/client/components/modal/modals/Settings.tsx index dd6c077721..aac8b8457a 100644 --- a/packages/client/components/modal/modals/Settings.tsx +++ b/packages/client/components/modal/modals/Settings.tsx @@ -20,7 +20,7 @@ export function SettingsModal( const config = SettingsConfigurations[props.config]; //Drawer slider for mobile - let rootRef, sDrawer: SlideDrawer | null; + let rootRef, sDrawer: SlideDrawer | undefined; const [contRef, setContRef] = createSignal(); createEffect( on(contRef, (cont) => { @@ -31,7 +31,7 @@ export function SettingsModal( ); onCleanup(() => { sDrawer?.delete(); - setDiagDrawer((sDrawer = null)); + setDiagDrawer((sDrawer = undefined)); }); return ( diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx index 6f6db4dd0a..83f5e746a4 100644 --- a/packages/client/components/state/index.tsx +++ b/packages/client/components/state/index.tsx @@ -115,11 +115,11 @@ export class State { this.writeQueue = {}; this.isMobile = isMobileBrowser(); - const [ad, setAd] = createSignal(); + const [ad, setAd] = createSignal(); this.appDrawer = ad; this.setAppDrawer = setAd; - const [dd, setDd] = createSignal(); + const [dd, setDd] = createSignal(); this.diagDrawer = dd; this.setDiagDrawer = setDd; } diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index 8d989633fc..e6ae9cde43 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -21,7 +21,7 @@ type TrackTouch = { }; export enum SlideState { - HIDDEN, + HIDDEN = 1, SHOWN, HIDING, SHOWING, diff --git a/packages/client/src/Interface.tsx b/packages/client/src/Interface.tsx index e53514c748..7b81b4826c 100644 --- a/packages/client/src/Interface.tsx +++ b/packages/client/src/Interface.tsx @@ -63,7 +63,7 @@ const Interface = (props: { children: JSX.Element }) => { } //Drawer slider for mobile - let rootRef, sDrawer: SlideDrawer | null; + let rootRef, sDrawer: SlideDrawer | undefined; const [contRef, setContRef] = createSignal(); function rstLayout() { state.layout.setSectionState(LAYOUT_SECTIONS.PRIMARY_SIDEBAR, false, false); @@ -77,14 +77,14 @@ const Interface = (props: { children: JSX.Element }) => { if (sDrawer) { const en = sDrawer.enabled; setTimeout(() => { - state.setAppDrawer(en ? sDrawer : null); + state.setAppDrawer(en ? sDrawer : undefined); if (en) rstLayout(); }, 1); } }); onCleanup(() => { sDrawer?.delete(); - state.setAppDrawer((sDrawer = null)); + state.setAppDrawer((sDrawer = undefined)); }); return ( From f9bf236300d4ea88220ec0517babc7baada6ed5e Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Fri, 8 May 2026 03:28:58 -0400 Subject: [PATCH 29/34] fix: Fixes for VoiceCallCardPiP Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- .../components/features/voice/callCard/VoiceCallCard.tsx | 7 ++++++- .../features/voice/callCard/VoiceCallCardActions.tsx | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 4ad116a949..9ac421df9d 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -18,6 +18,8 @@ import { Channel } from "stoat.js"; import { styled } from "styled-system/jsx"; import { useVoice } from "@revolt/rtc"; +import { useState } from "@revolt/state"; +import { SlideState } from "@revolt/ui/components/navigation/SlideDrawer"; import { VoiceCallCardActiveRoom } from "./VoiceCallCardActiveRoom"; import { VoiceCallCardPiP } from "./VoiceCallCardPiP"; @@ -29,6 +31,7 @@ type FloatType = "tl" | "tr" | "bl" | "br"; type Info = { channel: Channel; pos: DOMRect; + drawer?: SlideState; }; const PAD = 16, @@ -103,7 +106,7 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { const sty = ref.style; //Set mode based on state - if (inf?.pos) { + if (inf?.pos && (!inf.drawer || inf.drawer === SlideState.SHOWN)) { sty.transform = `translate(${inf.pos.x}px, ${inf.pos.y}px)`; sty.width = `${inf.pos.width}px`; setMode(); @@ -181,6 +184,7 @@ const Float = styled("div", { /** 'Marker' to send position information for mounting the floating call card */ export function VoiceChannelCallCardMount(props: { channel: Channel }) { const voice = useVoice(); + const state = useState(); const setInfo = useContext(callCardContext)!; let ref: HTMLDivElement | undefined; @@ -191,6 +195,7 @@ export function VoiceChannelCallCardMount(props: { channel: Channel }) { ? { channel: props.channel, pos: ref!.getBoundingClientRect(), + drawer: state.appDrawer()?.state, } : undefined, ); diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index 8c3cfa0492..fcf6e75dc4 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -6,11 +6,13 @@ import { styled } from "styled-system/jsx"; import { CONFIGURATION } from "@revolt/common"; import { useVoice } from "@revolt/rtc"; +import { useState } from "@revolt/state"; import { Button, IconButton } from "@revolt/ui/components/design"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { const voice = useVoice(); + const state = useState(); const navigate = useNavigate(); const { t } = useLingui(); @@ -24,6 +26,7 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { size={props.size} onPress={() => { navigate(voice.channel()?.path ?? ""); + state.appDrawer()?.setShown(true); }} use:floating={{ tooltip: { From c067773beec733ba742a6121cd53e744145d70af Mon Sep 17 00:00:00 2001 From: Pecacheu <3608878+Pecacheu@users.noreply.github.com> Date: Sun, 24 May 2026 00:22:20 -0400 Subject: [PATCH 30/34] refactor: Use new resize & layout hooks Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/state/index.tsx | 4 -- .../composition/picker/EmojiPicker.tsx | 4 +- .../composition/picker/GifPicker.tsx | 4 +- .../features/messaging/elements/Container.tsx | 4 +- .../features/texteditor/TextEditor2.tsx | 4 +- .../ui/components/floating/UserCard.tsx | 4 +- .../ui/components/navigation/SlideDrawer.ts | 54 +++++++++++-------- packages/client/components/ui/styles.css | 1 - .../navigation/channels/HomeSidebar.tsx | 4 +- .../navigation/channels/ServerSidebar.tsx | 5 +- 10 files changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/client/components/state/index.tsx b/packages/client/components/state/index.tsx index 83f5e746a4..f33efea18d 100644 --- a/packages/client/components/state/index.tsx +++ b/packages/client/components/state/index.tsx @@ -11,7 +11,6 @@ import { SetStoreFunction, createStore } from "solid-js/store"; import equal from "fast-deep-equal"; import localforage from "localforage"; -import { isMobileBrowser } from "@livekit/components-core"; import { SlideDrawer } from "@revolt/ui/components/navigation/SlideDrawer"; import { AbstractStore, Store } from "./stores"; import { Auth } from "./stores/Auth"; @@ -53,7 +52,6 @@ export class State { private setStore: SetStoreFunction; private writeQueue: Record; - isMobile: boolean; appDrawer; setAppDrawer; diagDrawer; @@ -109,11 +107,9 @@ export class State { */ constructor() { const [store, setStore] = createStore(this.defaults() as Store); - this.store = store as never; this.setStore = setStore; this.writeQueue = {}; - this.isMobile = isMobileBrowser(); const [ad, setAd] = createSignal(); this.appDrawer = ad; diff --git a/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx b/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx index 79d05ac012..8e2dcc05ba 100644 --- a/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx +++ b/packages/client/components/ui/components/features/messaging/composition/picker/EmojiPicker.tsx @@ -14,6 +14,7 @@ import { cva } from "styled-system/css"; import { styled } from "styled-system/jsx"; import { useClient } from "@revolt/client"; +import { useDevice } from "@revolt/common"; import { UnicodeEmoji } from "@revolt/markdown/emoji"; import { UNICODE_EMOJI_PACK_PUA } from "@revolt/markdown/emoji/UnicodeEmoji"; import { useState } from "@revolt/state"; @@ -67,6 +68,7 @@ type Item = export function EmojiPicker() { const client = useClient(); const state = useState(); + const { isMobile } = useDevice(); const [filter, setFilter] = createSignal(""); const [colCount, setColCount] = createSignal(0); @@ -145,7 +147,7 @@ export function EmojiPicker() { return ( void>(); export function GifPicker() { - const { isMobile } = useState(); + const { isMobile } = useDevice(); const [filter, setFilter] = createSignal(""); const fliterLowercase = () => filter().toLowerCase(); diff --git a/packages/client/components/ui/components/features/messaging/elements/Container.tsx b/packages/client/components/ui/components/features/messaging/elements/Container.tsx index d522a85936..16b2e90385 100644 --- a/packages/client/components/ui/components/features/messaging/elements/Container.tsx +++ b/packages/client/components/ui/components/features/messaging/elements/Container.tsx @@ -5,6 +5,7 @@ import { Message } from "stoat.js"; import { cva } from "styled-system/css"; import { styled } from "styled-system/jsx"; +import { useDevice } from "@revolt/common"; import { Ripple, typography } from "@revolt/ui/components/design"; import { Column, Row } from "@revolt/ui/components/layout"; import { @@ -13,7 +14,6 @@ import { Time, } from "@revolt/ui/components/utils"; -import { useState } from "@revolt/state"; import { MediaPickerProps } from "../composition/picker/CompositionMediaPicker"; import { MessageToolbar } from "./MessageToolbar"; @@ -313,7 +313,7 @@ const CompactInfo = styled(Row, { */ export function MessageContainer(props: Props) { const { t } = useLingui(); - const { isMobile } = useState(); + const { isMobile } = useDevice(); return (
void }, ) { - const { isMobile } = useState(); + const { isMobile } = useDevice(); const { openModal } = useModals(); const query = useQuery(() => ({ queryKey: ["profile", props.user.id], diff --git a/packages/client/components/ui/components/navigation/SlideDrawer.ts b/packages/client/components/ui/components/navigation/SlideDrawer.ts index e6ae9cde43..1180c3d879 100644 --- a/packages/client/components/ui/components/navigation/SlideDrawer.ts +++ b/packages/client/components/ui/components/navigation/SlideDrawer.ts @@ -1,4 +1,12 @@ -import { createSignal } from "solid-js"; +import { + Accessor, + createEffect, + createRoot, + createSignal, + Setter, +} from "solid-js"; + +import { useDevice } from "@revolt/common"; const ANIM_MS = 150, VEL_MS = 33, //30Hz velocity update @@ -29,16 +37,16 @@ export enum SlideState { } export class SlideDrawer { - private media; private touch: TrackTouch | null = null; private tTmr: NodeJS.Timeout | null = null; private vTmr: NodeJS.Timeout | null = null; private ofs = 0; - private eGet; - private eSet; - private sGet; - private sSet; + private dispose!: () => void; + private eGet!: Accessor; + private eSet!: Setter; + private sGet!: Accessor; + private sSet!: Setter; constructor( private drawer: HTMLElement, @@ -50,21 +58,23 @@ export class SlideDrawer { root.addEventListener("touchmove", this.move); root.addEventListener("touchend", this.move); - //Signals - const [eg, es] = createSignal(false); - this.eGet = eg; - this.eSet = es; - const [sg, ss] = createSignal(SlideState.HIDDEN); - this.sGet = sg; - this.sSet = ss; - - //Auto-enable based on device width - const pwMax = getComputedStyle(document.body).getPropertyValue( - "--phone-max-width", - ); - this.media = matchMedia(`(max-width: ${pwMax})`); - this.media.onchange = (e) => (this.enabled = e.matches); - this.enabled = this.media.matches; + createRoot((dispose) => { + this.dispose = dispose; + + //Signals + const [eg, es] = createSignal(false); + this.eGet = eg; + this.eSet = es; + const [sg, ss] = createSignal(SlideState.HIDDEN); + this.sGet = sg; + this.sSet = ss; + + //Auto-enable based on layout + const dev = useDevice(); + createEffect(() => { + this.enabled = dev.layout() === "phone"; + }); + }); } private start(e: TouchEvent) { @@ -210,7 +220,7 @@ export class SlideDrawer { this.root.removeEventListener("touchstart", this.start); this.root.removeEventListener("touchmove", this.move); this.root.removeEventListener("touchend", this.move); - this.media.onchange = null; + this.dispose(); } get enabled() { diff --git a/packages/client/components/ui/styles.css b/packages/client/components/ui/styles.css index c62738d11c..75cf4ce2d3 100644 --- a/packages/client/components/ui/styles.css +++ b/packages/client/components/ui/styles.css @@ -17,7 +17,6 @@ body { margin: 0; background: #191919; font-family: var(--fonts-primary); - --phone-max-width: 600px; } #root { diff --git a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx index 9b6d87c106..a7b7f03939 100644 --- a/packages/client/src/interface/navigation/channels/HomeSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/HomeSidebar.tsx @@ -25,7 +25,7 @@ import { Symbol } from "@revolt/ui/components/utils/Symbol"; import MdClose from "@material-design-icons/svg/outlined/close.svg?component-solid"; -import { useState } from "@revolt/state"; +import { useDevice } from "@revolt/common"; import { SidebarBase } from "./common"; interface Props { @@ -56,7 +56,7 @@ export const HomeSidebar = (props: Props) => { const navigate = useNavigate(); const location = useLocation(); const { openModal } = useModals(); - const { isMobile } = useState(); + const { isMobile } = useDevice(); const savedNotesChannelId = createMemo(() => props.openSavedNotes()); diff --git a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx index f683bdb215..0e705ecc93 100644 --- a/packages/client/src/interface/navigation/channels/ServerSidebar.tsx +++ b/packages/client/src/interface/navigation/channels/ServerSidebar.tsx @@ -13,6 +13,7 @@ import { useLingui } from "@lingui-solid/solid/macro"; import type { API, Channel, Server, ServerFlags } from "stoat.js"; import { styled } from "styled-system/jsx"; +import { useDevice } from "@revolt/common"; import { KeybindAction, createKeybind } from "@revolt/keybinds"; import { TextWithEmoji } from "@revolt/markdown"; import { useModals } from "@revolt/modal"; @@ -37,7 +38,6 @@ import { createDragHandle } from "@revolt/ui/components/utils/Draggable"; import { Symbol } from "@revolt/ui/components/utils/Symbol"; import MdChevronRight from "@material-design-icons/svg/filled/chevron_right.svg?component-solid"; - import MdSettings from "@material-symbols/svg-400/outlined/settings-fill.svg?component-solid"; import { SidebarBase } from "./common"; @@ -451,6 +451,7 @@ function Entry( const state = useState(); const voice = useVoice(); const { openModal } = useModals(); + const { isMobile } = useDevice(); const canEditChannel = createMemo(() => (["ManageChannel", "ManagePermissions", "ManageWebhooks"] as const).some( @@ -511,7 +512,7 @@ function Entry( } actions={ - + Date: Mon, 25 May 2026 21:04:19 -0400 Subject: [PATCH 31/34] fix: Apply mobile login layout using hook Signed-off-by: Pecacheu <3608878+Pecacheu@users.noreply.github.com> --- packages/client/components/auth/src/AuthPage.tsx | 4 +++- packages/client/components/auth/src/flows/Flow.tsx | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/client/components/auth/src/AuthPage.tsx b/packages/client/components/auth/src/AuthPage.tsx index 9c08a8fdec..3df31e4512 100644 --- a/packages/client/components/auth/src/AuthPage.tsx +++ b/packages/client/components/auth/src/AuthPage.tsx @@ -5,6 +5,7 @@ import { Trans } from "@lingui-solid/solid/macro"; import { styled } from "styled-system/jsx"; import { Titlebar } from "@revolt/app/interface/desktop/Titlebar"; +import { useDevice } from "@revolt/common"; import { useState } from "@revolt/state"; import { IconButton, iconSize } from "@revolt/ui"; @@ -117,6 +118,7 @@ const Bullet = styled("div", { */ export function AuthPage(props: { children: JSX.Element }) { const state = useState(); + const { layout } = useDevice(); return (
- {props.children} + {props.children}