Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-modal-for-room-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Added a modal for viewing room details on mobile.
5 changes: 5 additions & 0 deletions .changeset/fix-following-conversation-press-animation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Replaced the shrinking scale animation on the "following the conversation" press with an opacity dim.
5 changes: 5 additions & 0 deletions .changeset/fix-mobile-touch-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Improved mobile touch feel, avoiding ghost presses and removing timeline message highlighting from touch inputs.
5 changes: 5 additions & 0 deletions .changeset/fix-sliding-sync-not-syncing-calls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix sliding sync not subscribing to call updates.
3 changes: 3 additions & 0 deletions .github/scripts/set-tauri-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ if (updaterEndpoint && !updaterEndpoint.startsWith('https://')) {
const file = 'src-tauri/tauri.conf.json';
const config = JSON.parse(readFileSync(file, 'utf8'));
config.version = version;
if (version.includes('nightly')) {
config.productName = 'Sable Nightly';
}
if (updaterEndpoint) {
config.plugins.updater.endpoints = [updaterEndpoint];
}
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/tauri-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ jobs:
env:
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }}
VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }}
NODE_OPTIONS: --max-old-space-size=8192
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
Expand Down Expand Up @@ -292,6 +294,8 @@ jobs:
env:
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }}
VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }}
NODE_OPTIONS: --max-old-space-size=8192
ANDROID_KEY_BASE64: ${{ secrets.ANDROID_KEY_BASE64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
Expand Down Expand Up @@ -382,6 +386,8 @@ jobs:
env:
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }}
VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }}
NODE_OPTIONS: --max-old-space-size=8192
steps:
- name: Checkout repository
Expand Down Expand Up @@ -468,6 +474,8 @@ jobs:
env:
TAG: ${{ needs.setup-release.outputs.tag }}
VERSION: ${{ needs.setup-release.outputs.version }}
VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }}
VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }}
REPO: ${{ github.repository }}
STARTED_AT: ${{ needs.setup-release.outputs.started_at }}
steps:
Expand Down
3 changes: 3 additions & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export default defineConfig({
allow: [
'_fetched',
'__dirname',
'__filename',
'__sableAndroidBack',
'__SABLE_PRELOAD',
'__WB_MANIFEST',
'_unstable_sendDelayedEvent',
'_unstable_getDelayedEvents',
Expand Down
14 changes: 10 additions & 4 deletions scripts/cef-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,24 @@ STAGE="$ROOT/src-tauri/target/release"
OUT="$STAGE/bundle"
WORK="$STAGE/cef-pkg"

[ -x "$STAGE/sable" ] || {
echo "missing $STAGE/sable; build it first (pnpm tauri:cef build)" >&2
if [ -x "$STAGE/Sable Nightly" ]; then
BIN_NAME="Sable Nightly"
elif [ -x "$STAGE/Sable" ]; then
BIN_NAME="Sable"
elif [ -x "$STAGE/sable" ]; then
BIN_NAME="sable"
else
echo "missing $STAGE/Sable or sable; build it first (pnpm tauri:cef build)" >&2
exit 1
}
fi

rm -rf "$WORK"
mkdir -p "$WORK" "$OUT/deb" "$OUT/rpm" "$OUT/appimage"

stage_runtime() {
local dest="$1"
mkdir -p "$dest"
cp "$STAGE/sable" "$dest/"
cp "$STAGE/$BIN_NAME" "$dest/sable"
bash scripts/cef-copy-libs.sh release "$dest"
}

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "sable",
"productName": "Sable",
"version": "1.20.0",
"identifier": "moe.sable.client",
"build": {
Expand Down
91 changes: 91 additions & 0 deletions src/app/components/MobileSwipeDownModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useRef, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { Box } from 'folds';
import * as css from '$features/room/message/styles.css';

interface MobileSwipeDownModalProps {
children: (
dragHandle: React.ReactNode,
dragHandlers: {
onTouchStart: (e: React.TouchEvent) => void;
onTouchMove: (e: React.TouchEvent) => void;
onTouchEnd: () => void;
}
) => React.ReactNode;
requestClose: () => void;
}

export function MobileSwipeDownModal({ children, requestClose }: MobileSwipeDownModalProps) {
const touchStartY = useRef<number | null>(null);
const [touchYDiff, setTouchYDiff] = useState(0);
const date = new Date();
const startTime = useRef(0);
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

const handleTouchStart = (e: React.TouchEvent) => {
touchStartY.current = e.touches[0]?.clientY ?? null;
startTime.current = date.getTime();
};

const handleTouchMove = (e: React.TouchEvent) => {
if (touchStartY.current === null || !e.touches[0]) return;
const touchY = e.touches[0].clientY;
const diff = touchY - touchStartY.current;

// Only allow pulling down
if (diff > 0) {
setTouchYDiff(diff);
}
};

const handleTouchEnd = () => {
const endTime = date.getTime();
if (touchYDiff > 100 || (endTime - startTime.current < 600 && touchYDiff > 20)) {
requestClose();
} else {
setTouchYDiff(0);
}
touchStartY.current = null;
};

const dragHandlers = {
onTouchStart: handleTouchStart,
onTouchMove: handleTouchMove,
onTouchEnd: handleTouchEnd,
};

const dragHandleJSX = (
<div className={css.MessageMobileDragHandle} {...dragHandlers}>
<div className={css.MessageMobileDragIndicator} />
</div>
);

if (!mounted) return null;

return createPortal(
<Box
className={css.MessageMobileOptionsWrapped}
data-gestures="ignore"
onClick={requestClose}
onTouchStart={(e: React.TouchEvent) => e.stopPropagation()}
onTouchMove={(e: React.TouchEvent) => e.stopPropagation()}
onTouchEnd={(e: React.TouchEvent) => e.stopPropagation()}
>
<Box
className={css.MessageMobileOptionsContainer}
style={{
transform: touchYDiff > 0 ? `translateY(${touchYDiff}px)` : undefined,
transition: touchStartY.current === null ? 'transform 0.2s ease-out' : 'none',
}}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
{children(dragHandleJSX, dragHandlers)}
</Box>
</Box>,
document.body
);
}
6 changes: 4 additions & 2 deletions src/app/components/SwipeableChatWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export function SwipeableChatWrapper({
const x = useMotionValue(0);

const bind = useDrag(
({ first, active, offset: [ox], velocity: [vx], direction: [dx], event: e }) => {
if (e && 'target' in e && e.target instanceof HTMLElement) {
({ first, active, offset: [ox], velocity: [vx], direction: [dx], event: e, cancel }) => {
if (first && e && 'target' in e && e.target instanceof HTMLElement) {
if (e.target.closest('[data-gestures="ignore"]')) {
cancel();
return;
}
}
Expand Down Expand Up @@ -65,6 +66,7 @@ export function SwipeableChatWrapper({
rubberband: true,
filterTaps: true,
pointer: { capture: false },
eventOptions: { passive: true },
from: () => [x.get(), 0],
}
);
Expand Down
6 changes: 4 additions & 2 deletions src/app/components/SwipeableOverlayWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export function SwipeableOverlayWrapper({
const x = useMotionValue(0);

const bind = useDrag(
({ first, active, offset: [ox], velocity: [vx], direction: [dx], event }) => {
if (event && 'target' in event && event.target instanceof HTMLElement) {
({ first, active, offset: [ox], velocity: [vx], direction: [dx], event, cancel }) => {
if (first && event && 'target' in event && event.target instanceof HTMLElement) {
if (event.target.closest('[data-gestures="ignore"]')) {
cancel();
return;
}
}
Expand Down Expand Up @@ -62,6 +63,7 @@ export function SwipeableOverlayWrapper({
rubberband: true,
filterTaps: true,
pointer: { capture: true },
eventOptions: { passive: true },
from: () => [x.get(), 0],
}
);
Expand Down
88 changes: 14 additions & 74 deletions src/app/components/message/modals/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import {
import { MessageEditHistoryItem } from './MessageEditHistory';
import { MessageSourceCodeItem } from './MessageSource';
import { MessageForwardItem } from './MessageForward';
import { MobileSwipeDownModal } from '$components/MobileSwipeDownModal';

import * as css from '$features/room/message/styles.css';
import { useAtom, useSetAtom, useStore } from 'jotai';
import type { Dispatch, MouseEventHandler, ReactNode, SetStateAction } from 'react';
import { useCallback, useEffect, useState, useRef } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { MessageDeleteItem } from './MessageDelete';
import FocusTrap from 'focus-trap-react';
import { stopPropagation } from '$utils/keyboard';
Expand Down Expand Up @@ -833,89 +834,25 @@ export function OptionMenu({
export function MobileOptionsInternal({ options }: { options: OptionMenuProps }) {
const [isActive, setIsActive] = useState(true);
const [modal, setModal] = useAtom(modalAtom);
const touchStartY = useRef<number | null>(null);
const [touchYDiff, setTouchYDiff] = useState(0);
const date = new Date();
const startTime = useRef(0);

useEffect(() => {
if (modal?.type === ModalType.MobileOptions) setIsActive(true);
if (!isActive) setModal(null);
}, [modal, setIsActive, isActive, setModal]);

const handleTouchStart = (e: React.TouchEvent) => {
touchStartY.current = e.touches[0]?.clientY ?? null;
startTime.current = date.getTime();
};

const handleTouchMove = (e: React.TouchEvent) => {
if (touchStartY.current === null || !e.touches[0]) return;
const touchY = e.touches[0].clientY;
const diff = touchY - touchStartY.current;

// Only allow pulling down
if (diff > 0) {
setTouchYDiff(diff);
}
};

const handleTouchEnd = () => {
const endTime = date.getTime();
if (touchYDiff > 100 || (endTime - startTime.current < 600 && touchYDiff > 20)) {
options.closeMenu();
setIsActive(false);
} else {
setTouchYDiff(0);
}
touchStartY.current = null;
};

const dragHandleJSX = (
<div
className={css.MessageMobileDragHandle}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div className={css.MessageMobileDragIndicator} />
</div>
);

const dragOpts: DragOptsProps = {
dragHandle: dragHandleJSX,
onTouchStart: handleTouchStart,
onTouchMove: handleTouchMove,
onTouchEnd: handleTouchEnd,
const requestClose = () => {
options.closeMenu();
setIsActive(false);
};

if (isActive)
return (
<Box
className={css.MessageMobileOptionsWrapped}
data-gestures="ignore"
onClick={() => {
options.closeMenu();
setIsActive(false);
}}
onTouchStart={(e: React.TouchEvent) => e.stopPropagation()}
onTouchMove={(e: React.TouchEvent) => e.stopPropagation()}
onTouchEnd={(e: React.TouchEvent) => e.stopPropagation()}
>
<Box
className={css.MessageMobileOptionsContainer}
style={{
transform: touchYDiff > 0 ? `translateY(${touchYDiff}px)` : undefined,
transition: touchStartY.current === null ? 'transform 0.2s ease-out' : 'none',
}}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
>
<MobileSwipeDownModal requestClose={requestClose}>
{(dragHandleJSX, dragHandlers) => (
<OptionMenu
mEvent={options.mEvent}
room={options.room}
closeMenu={() => {
options.closeMenu();
setIsActive(false);
}}
closeMenu={requestClose}
onReactionToggle={options.onReactionToggle}
relations={options.relations}
onReplyClick={options.onReplyClick}
Expand All @@ -928,11 +865,14 @@ export function MobileOptionsInternal({ options }: { options: OptionMenuProps })
ActualMessage={options.ActualMessage}
canSendReaction={options.canSendReaction}
isModal
dragOpts={dragOpts}
dragOpts={{
dragHandle: dragHandleJSX,
...dragHandlers,
}}
isGif={options.isGif}
/>
</Box>
</Box>
)}
</MobileSwipeDownModal>
);
return <></>;
}
3 changes: 2 additions & 1 deletion src/app/components/page/MobileNavDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra
if (!mobileGestures || width === 0) return;

const target = event?.target;
if (target instanceof HTMLElement && target.closest('[data-gestures="ignore"]')) {
if (first && target instanceof HTMLElement && target.closest('[data-gestures="ignore"]')) {
cancel();
return;
}
Expand Down Expand Up @@ -275,6 +275,7 @@ export function MobileNavDrawer({ nav, rail, bottomNav, children }: MobileNavDra
filterTaps: true,
tapsThreshold: DIRECTION_DEADZONE,
pointer: { capture: false },
eventOptions: { passive: true },
from: () => [readX(), 0],
}
);
Expand Down
Loading
Loading