Compare commits

...

9 Commits

Author SHA1 Message Date
GitHub Actions
70d6f6ff42 chore: bump version to 4.11.123 [skip ci] 2026-07-11 11:24:56 +00:00
97f0a15ae2 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-android (push) Successful in 4m50s
Build / create-release (push) Successful in 14s
2026-07-11 21:24:47 +10:00
4cf19131bf chore: mark subproject as dirty and update package-lock.json with new version and dependencies 2026-07-11 21:24:39 +10:00
GitHub Actions
83137c92a9 chore: bump version to 4.11.122 [skip ci] 2026-07-11 11:18:23 +00:00
426ddf5650 Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
Some checks failed
Build / increment-version (push) Successful in 7s
Build / build-android (push) Failing after 1m0s
Build / create-release (push) Has been skipped
2026-07-11 21:18:12 +10:00
7800e7ced7 refactor: implement immersive mode for mobile keyboard layout and update related styles 2026-07-11 21:18:06 +10:00
GitHub Actions
a5afa6b493 chore: bump version to 4.11.121 [skip ci] 2026-07-11 11:03:38 +00:00
afeb45f59d Merge branch 'master' of http://synbox.ruv.wtf:8418/litruv/cinny-mobile
All checks were successful
Build / increment-version (push) Successful in 7s
Build / build-android (push) Successful in 5m9s
Build / create-release (push) Successful in 14s
2026-07-11 21:03:28 +10:00
6f45f94f19 refactor: adjust RoomView component for improved mobile composer behavior and styling 2026-07-11 21:03:18 +10:00
10 changed files with 273 additions and 14 deletions

View File

@@ -1,3 +1,5 @@
{ {
"dependencies": {} "dependencies": {
"@capacitor/status-bar": "8.0.1"
}
} }

View File

@@ -0,0 +1,79 @@
import { style } from '@vanilla-extract/css';
import { color, config, DefaultReset, toRem } from 'folds';
export const Editor = style([
DefaultReset,
{
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
borderRadius: config.radii.R400,
overflow: 'hidden',
selectors: {
// Keyboard-flush dock: only a top edge so the input can sit on the IME.
'[data-composer-flush="true"] &': {
borderRadius: 0,
boxShadow: `inset 0 ${config.borderWidth.B300} 0 0 ${color.SurfaceVariant.ContainerLine}`,
},
},
},
]);
export const EditorOptions = style([
DefaultReset,
{
padding: config.space.S200,
},
]);
export const EditorTextareaScroll = style({});
export const EditorTextarea = style([
DefaultReset,
{
flexGrow: 1,
height: '100%',
padding: `${toRem(13)} ${toRem(1)}`,
selectors: {
[`${EditorTextareaScroll}:first-child &`]: {
paddingLeft: toRem(13),
},
[`${EditorTextareaScroll}:last-child &`]: {
paddingRight: toRem(13),
},
'&:focus': {
outline: 'none',
},
},
},
]);
export const EditorPlaceholderContainer = style([
DefaultReset,
{
opacity: config.opacity.Placeholder,
pointerEvents: 'none',
userSelect: 'none',
},
]);
export const EditorPlaceholderTextVisual = style([
DefaultReset,
{
display: 'block',
paddingTop: toRem(13),
paddingLeft: toRem(1),
},
]);
export const EditorToolbarBase = style({
padding: `0 ${config.borderWidth.B300}`,
});
export const EditorToolbar = style({
padding: config.space.S100,
});
export const MarkdownBtnBox = style({
paddingRight: config.space.S100,
});

View File

@@ -27,6 +27,7 @@ import { activeThreadIdAtomFamily } from '../../state/activeThread';
import { ThreadView } from './ThreadView'; import { ThreadView } from './ThreadView';
import { MobileSwipeToReplyLayer } from '../../components/mobile/MobileSwipeToReplyLayer'; import { MobileSwipeToReplyLayer } from '../../components/mobile/MobileSwipeToReplyLayer';
import { useMobileKeyboardLayout } from '../../hooks/useMobileKeyboardLayout'; import { useMobileKeyboardLayout } from '../../hooks/useMobileKeyboardLayout';
import * as composerCss from './room-composer-mobile.css';
const FN_KEYS_REGEX = /^F\d+$/; const FN_KEYS_REGEX = /^F\d+$/;
const shouldFocusMessageField = (evt: KeyboardEvent): boolean => { const shouldFocusMessageField = (evt: KeyboardEvent): boolean => {
@@ -64,7 +65,7 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
const roomViewRef = useRef<HTMLDivElement>(null); const roomViewRef = useRef<HTMLDivElement>(null);
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const { isLandscape, keyboardOpen } = useMobileKeyboardLayout(); const { immersive } = useMobileKeyboardLayout();
const { roomId } = room; const { roomId } = room;
const editor = useEditor(); const editor = useEditor();
@@ -103,8 +104,9 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
) )
); );
const composerPadding = isLandscape ? '0' : `0 ${config.space.S400}`; const flushComposer = immersive;
const showHeader = !keyboardOpen; const composerPadding = flushComposer ? '0' : `0 ${config.space.S400}`;
const showHeader = !immersive;
return ( return (
<Page ref={roomViewRef}> <Page ref={roomViewRef}>
@@ -125,7 +127,20 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
<RoomViewTyping room={room} /> <RoomViewTyping room={room} />
</Box> </Box>
</MobileSwipeToReplyLayer> </MobileSwipeToReplyLayer>
<Box shrink="No" direction="Column" data-disable-swipe-back="true"> <Box
shrink="No"
direction="Column"
data-disable-swipe-back="true"
data-composer-flush={flushComposer ? 'true' : undefined}
className={composerCss.ComposerDock}
>
{immersive && !hideActivity && (
<div className={composerCss.FollowingFloat}>
<div className={composerCss.FollowingFloatHit}>
<RoomViewFollowing room={room} />
</div>
</div>
)}
<div style={{ padding: composerPadding }}> <div style={{ padding: composerPadding }}>
{tombstoneEvent ? ( {tombstoneEvent ? (
<RoomTombstone <RoomTombstone
@@ -156,7 +171,12 @@ export function RoomView({ room, eventId }: { room: Room; eventId?: string }) {
</> </>
)} )}
</div> </div>
{hideActivity ? <RoomViewFollowingPlaceholder /> : <RoomViewFollowing room={room} />} {!immersive &&
(hideActivity ? (
<RoomViewFollowingPlaceholder />
) : (
<RoomViewFollowing room={room} />
))}
</Box> </Box>
</> </>
)} )}

View File

@@ -0,0 +1,49 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { DefaultReset, color, config, toRem } from 'folds';
export const RoomViewFollowingPlaceholder = style([
DefaultReset,
{
height: toRem(28),
},
]);
export const RoomViewFollowing = recipe({
base: [
DefaultReset,
{
minHeight: toRem(28),
padding: `0 ${config.space.S400}`,
width: '100%',
backgroundColor: color.Surface.Container,
color: color.Surface.OnContainer,
outline: 'none',
selectors: {
// Compact floating chip while the keyboard is open.
'[data-composer-flush="true"] &': {
minHeight: 0,
width: 'auto',
maxWidth: '100%',
padding: `${config.space.S100} ${config.space.S200}`,
backgroundColor: 'transparent',
},
},
},
],
variants: {
clickable: {
true: {
cursor: 'pointer',
selectors: {
'&:hover, &:focus-visible': {
color: color.Primary.Main,
},
'&:active': {
color: color.Primary.Main,
},
},
},
},
},
});

View File

@@ -0,0 +1,63 @@
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { DefaultReset, color, config } from 'folds';
export const ComposerDock = style({
position: 'relative',
});
/** Read receipts float above the input so they don't reserve layout height. */
export const FollowingFloat = style({
position: 'absolute',
bottom: '100%',
left: 0,
right: 0,
zIndex: 2,
display: 'flex',
justifyContent: 'flex-end',
pointerEvents: 'none',
padding: `0 ${config.space.S200}`,
});
export const FollowingFloatHit = style({
pointerEvents: 'auto',
maxWidth: '100%',
});
export const RoomViewFollowingPlaceholder = style([
DefaultReset,
{
display: 'none',
height: 0,
},
]);
export const RoomViewFollowing = recipe({
base: [
DefaultReset,
{
minHeight: 0,
padding: `${config.space.S100} ${config.space.S200}`,
width: 'auto',
maxWidth: '100%',
backgroundColor: 'transparent',
color: color.Surface.OnContainer,
outline: 'none',
},
],
variants: {
clickable: {
true: {
cursor: 'pointer',
selectors: {
'&:hover, &:focus-visible': {
color: color.Primary.Main,
},
'&:active': {
color: color.Primary.Main,
},
},
},
},
},
});

View File

@@ -1,6 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { isCapacitorNative } from '../utils/tauri';
const KEYBOARD_HEIGHT_THRESHOLD_PX = 120; const KEYBOARD_HEIGHT_THRESHOLD_PX = 120;
const IMMERSIVE_CLASS = 'immersive-landscape-keyboard';
const getIsLandscape = (): boolean => { const getIsLandscape = (): boolean => {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@@ -24,9 +26,25 @@ const getKeyboardOpen = (): boolean => {
return tag === 'INPUT' || tag === 'TEXTAREA'; return tag === 'INPUT' || tag === 'TEXTAREA';
}; };
const setNativeStatusBarHidden = async (hidden: boolean) => {
if (!isCapacitorNative()) return;
try {
const { StatusBar } = await import('@capacitor/status-bar');
if (hidden) {
await StatusBar.hide();
} else {
await StatusBar.show();
}
} catch {
// Plugin may be missing in some builds; CSS safe-area collapse still applies.
}
};
export type MobileKeyboardLayout = { export type MobileKeyboardLayout = {
isLandscape: boolean; isLandscape: boolean;
keyboardOpen: boolean; keyboardOpen: boolean;
/** Landscape + keyboard: compact immersive room chrome. */
immersive: boolean;
}; };
/** /**
@@ -37,6 +55,8 @@ export const useMobileKeyboardLayout = (): MobileKeyboardLayout => {
const [isLandscape, setIsLandscape] = useState(getIsLandscape); const [isLandscape, setIsLandscape] = useState(getIsLandscape);
const [keyboardOpen, setKeyboardOpen] = useState(getKeyboardOpen); const [keyboardOpen, setKeyboardOpen] = useState(getKeyboardOpen);
const immersive = isLandscape && keyboardOpen;
useEffect(() => { useEffect(() => {
const sync = () => { const sync = () => {
setIsLandscape(getIsLandscape()); setIsLandscape(getIsLandscape());
@@ -63,5 +83,15 @@ export const useMobileKeyboardLayout = (): MobileKeyboardLayout => {
}; };
}, []); }, []);
return { isLandscape, keyboardOpen }; useEffect(() => {
document.documentElement.classList.toggle(IMMERSIVE_CLASS, immersive);
void setNativeStatusBarHidden(immersive);
return () => {
document.documentElement.classList.remove(IMMERSIVE_CLASS);
void setNativeStatusBarHidden(false);
};
}, [immersive]);
return { isLandscape, keyboardOpen, immersive };
}; };

View File

@@ -9,11 +9,11 @@ type ClientLayoutProps = {
}; };
export function ClientLayout({ nav, children }: ClientLayoutProps) { export function ClientLayout({ nav, children }: ClientLayoutProps) {
const { keyboardOpen } = useMobileKeyboardLayout(); const { immersive } = useMobileKeyboardLayout();
return ( return (
<Box grow="Yes"> <Box grow="Yes">
{!keyboardOpen && <Box shrink="No">{nav}</Box>} {!immersive && <Box shrink="No">{nav}</Box>}
<Box grow="Yes">{children}</Box> <Box grow="Yes">{children}</Box>
<DockedCallPanel /> <DockedCallPanel />
</Box> </Box>

View File

@@ -97,6 +97,11 @@ body {
font-variant-ligatures: no-contextual; font-variant-ligatures: no-contextual;
} }
/* Landscape + keyboard: reclaim status-bar inset while chrome is hidden. */
html.immersive-landscape-keyboard {
--safe-area-inset-top: 0px;
}
/* Prevent zalgo text and combining diacritics from overflowing line boundaries */ /* Prevent zalgo text and combining diacritics from overflowing line boundaries */
p, span, div, a, button, li, td, th, h1, h2, h3, h4, h5, h6 { p, span, div, a, button, li, td, th, h1, h2, h3, h4, h5, h6 {
line-height: 1.5; line-height: 1.5;

16
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{ {
"name": "paarrot", "name": "paarrot",
"version": "4.11.77", "version": "4.11.121",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "paarrot", "name": "paarrot",
"version": "4.11.77", "version": "4.11.121",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@capacitor/android": "8.3.0", "@capacitor/android": "8.3.0",
"@capacitor/cli": "8.3.0", "@capacitor/cli": "8.3.0",
"@capacitor/core": "8.3.0", "@capacitor/core": "8.3.0",
"@capacitor/local-notifications": "8.0.2" "@capacitor/local-notifications": "8.0.2",
"@capacitor/status-bar": "8.0.1"
}, },
"devDependencies": { "devDependencies": {
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
@@ -82,6 +83,15 @@
"@capacitor/core": ">=8.0.0" "@capacitor/core": ">=8.0.0"
} }
}, },
"node_modules/@capacitor/status-bar": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.1.tgz",
"integrity": "sha512-OR59dlbwvmrV5dKsC9lvwv48QaGbqcbSTBpk+9/WXWxXYSdXXdzJZU9p8oyNPAkuJhCdnSa3XmU43fZRPBJJ5w==",
"license": "MIT",
"peerDependencies": {
"@capacitor/core": ">=8.0.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "paarrot", "name": "paarrot",
"version": "4.11.120", "version": "4.11.123",
"description": "Paarrot - A Matrix client based on Cinny", "description": "Paarrot - A Matrix client based on Cinny",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -25,7 +25,8 @@
"@capacitor/android": "8.3.0", "@capacitor/android": "8.3.0",
"@capacitor/cli": "8.3.0", "@capacitor/cli": "8.3.0",
"@capacitor/core": "8.3.0", "@capacitor/core": "8.3.0",
"@capacitor/local-notifications": "8.0.2" "@capacitor/local-notifications": "8.0.2",
"@capacitor/status-bar": "8.0.1"
}, },
"devDependencies": { "devDependencies": {
"node-fetch": "3.3.2", "node-fetch": "3.3.2",