1
0
Fork 0
mirror of https://github.com/mat-1/variance.git synced 2025-08-02 15:26:04 +00:00

preserve room on reload and proper forward/backward room nav

This commit is contained in:
mat 2025-02-09 03:57:21 +00:00
parent 98d59ab0a2
commit 0333967290
14 changed files with 198 additions and 102 deletions

View file

@ -1,27 +1,25 @@
# Variance
A Matrix client that aims to be user-friendly and provide an experience similar to Discord. It's a hard-fork of [Cinny](https://github.com/cinnyapp/cinny).
A Matrix client that aims to be user-friendly and provide an experience similar to Discord.
It's a hard-fork of [Cinny](https://github.com/cinnyapp/cinny).
- [Contributing](./CONTRIBUTING.md)
## Notable features
- Markdown preview like Discord
- More keybinds like Discord (esc to focus input and scroll down, alt+arrow to navigate rooms, etc)
- More keybinds like Discord (esc to focus input and scroll down, alt+arrow to navigate rooms, back/forward navigation, etc)
- Better markdown support like Discord
- Indicator for when a message is still being sent, like Discord
- Threads
- Element-compatible theming
- Private read receipts
- Text reactions
## Getting started
A web app is available at https://variance.matdoes.dev and is updated on every commit.
To host Variance on your own, build it with `yarn build` and serve the `dist/` directory. To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
If you'd like to chat about Variance, you can join the Matrix space at [#variance:matdoes.dev](https://matrix.to/#/#variance:matdoes.dev).
## Local development
> We recommend using a version manager as versions change very quickly. You will likely need to switch
> between multiple Node.js versions based on the needs of different projects you're working on.

View file

@ -63,9 +63,9 @@ RoomSelectorWrapper.propTypes = {
interface RoomSelectorProps {
name: string;
parentName: string | null;
parentName?: string | null;
roomId: string;
imageSrc?: string;
imageSrc?: string | null;
iconSrc?: string;
isSelected: boolean;
isMuted: boolean;

View file

@ -65,6 +65,8 @@ function Drawer() {
});
}, [selectedTab]);
if (!roomList) return null;
return (
<div className="drawer">
<DrawerHeader selectedTab={selectedTab} spaceId={spaceId} />

View file

@ -23,7 +23,7 @@ interface SelectorProps {
roomId: string;
isDM?: boolean;
drawerPostie: {
subscribe: (channel: string, key: string, callback: () => void) => () => void;
subscribe: (_channel: string, _key: string, _callback: () => void) => () => void;
};
onClick: () => void;
}

View file

@ -57,11 +57,12 @@ function ProfileAvatarMenu() {
const mx = initMatrix.matrixClient;
const [profile, setProfile] = useState({
avatarUrl: null,
displayName: mx.getUser(mx.getUserId())?.displayName,
displayName: mx ? mx.getUser(mx.getUserId())?.displayName : undefined,
});
useEffect(() => {
const user = mx.getUser(mx.getUserId());
if (!mx) return () => {};
const user = mx.getUser(mx.getUserId()!)!;
const setNewProfile = (avatarUrl: string, displayName: string) =>
setProfile({
avatarUrl: avatarUrl || null,
@ -79,6 +80,8 @@ function ProfileAvatarMenu() {
};
}, []);
if (!mx) return null;
return (
<SidebarAvatar
onClick={openSettings}
@ -258,6 +261,8 @@ function DraggableSpaceShortcut({ isActive, spaceId, index, moveShortcut, onDrop
else shortcutRef.current.style.opacity = 1;
}
if (!room) return null;
return (
<SidebarAvatar
ref={shortcutRef}

View file

@ -7,7 +7,7 @@ import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
import RoomTimeline from '../../../client/state/RoomTimeline';
import navigation from '../../../client/state/navigation';
import { openNavigation } from '../../../client/action/navigation';
import { openNavigation, selectRoom } from '../../../client/action/navigation';
import Welcome from '../welcome/Welcome';
import RoomView from './RoomView';
@ -35,6 +35,8 @@ function Room() {
eventId: string | null,
threadId: string | null,
) => {
console.log('[select room] handleRoomSelected', roomId, mx.getRoom(roomId));
roomInfo.roomTimeline?.removeInternalListeners();
if (mx.getRoom(roomId)) {
const threadTimeline = threadId ? RoomTimeline.newFromThread(threadId, roomId) : null;
@ -66,6 +68,13 @@ function Room() {
};
}, []);
useEffect(() => {
// select the active room on load
const activeRoomId = localStorage.getItem(cons.ACTIVE_ROOM_ID);
if (activeRoomId) selectRoom(activeRoomId);
}, []);
const { roomTimeline, eventId } = roomInfo;
if (roomTimeline === null) {
setTimeout(() => openNavigation());

View file

@ -161,7 +161,7 @@ const tabItems = [
function RoomSettings({ roomId }) {
const [, forceUpdate] = useForceUpdate();
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const room = initMatrix.matrixClient.getRoom(roomId);
const room = initMatrix.matrixClient?.getRoom(roomId);
const handleTabChange = (tabItem) => {
setSelectedTab(tabItem);
@ -185,6 +185,7 @@ function RoomSettings({ roomId }) {
}, [forceUpdate]);
if (!navigation.isRoomSettings) return null;
if (!room) return null;
return (
<div className="room-settings">

View file

@ -36,7 +36,7 @@ import { useForceUpdate } from '../../hooks/useForceUpdate';
function RoomViewHeader({ roomId, threadId }: { roomId: string; threadId?: string }) {
const [, forceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId);
const isDM = initMatrix.roomList?.directs?.has(roomId);
const roomHeaderBtnRef = useRef(null);
useEffect(() => {
@ -63,6 +63,8 @@ function RoomViewHeader({ roomId, threadId }: { roomId: string; threadId?: strin
};
}, [roomId]);
if (!mx) return null;
const room = mx.getRoom(roomId);
if (!room) {
console.warn(`RoomViewHeader: Room ${roomId} not found`);

View file

@ -21,6 +21,7 @@ import cons from '../../../client/state/cons';
import DragDrop from '../../organisms/drag-drop/DragDrop';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import { selectRoom } from '../../../client/action/navigation';
function Client() {
const [isLoading, changeLoading] = useState(true);

View file

@ -2,25 +2,18 @@ import * as sdk from 'matrix-js-sdk';
import cons from '../state/cons';
import { WellKnown, getWellKnown } from '../../util/matrixUtil';
function updateLocalStore(
accessToken: string,
deviceId: string,
userId: string,
baseUrl: string,
slidingSyncProxyUrl?: string,
) {
function updateLocalStore(accessToken: string, deviceId: string, userId: string, baseUrl: string) {
localStorage.setItem(cons.secretKey.ACCESS_TOKEN, accessToken);
localStorage.setItem(cons.secretKey.DEVICE_ID, deviceId);
localStorage.setItem(cons.secretKey.USER_ID, userId);
localStorage.setItem(cons.secretKey.BASE_URL, baseUrl);
localStorage.setItem(cons.secretKey.SLIDING_SYNC_PROXY_URL, slidingSyncProxyUrl);
}
function createTemporaryClient(baseUrl) {
function createTemporaryClient(baseUrl: string) {
return sdk.createClient({ baseUrl });
}
async function startSsoLogin(baseUrl, type, idpId) {
async function startSsoLogin(baseUrl: string, type, idpId) {
const client = createTemporaryClient(baseUrl);
localStorage.setItem(cons.secretKey.BASE_URL, client.baseUrl);
window.location.href = client.getSsoLoginUrl(window.location.href, type, idpId);
@ -49,8 +42,7 @@ async function login(baseUrl: string, username: string, email: string, password:
// const wellKnown: WellKnown | undefined = res?.well_known;
const wellKnown: WellKnown = await getWellKnown(baseUrl);
const myBaseUrl = wellKnown?.['m.homeserver']?.base_url || client.baseUrl;
const mySlidingSyncProxyUrl = wellKnown?.['org.matrix.msc3575.proxy']?.url;
updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl, mySlidingSyncProxyUrl);
updateLocalStore(res.access_token, res.device_id, res.user_id, myBaseUrl);
}
async function loginWithToken(baseUrl, token) {

View file

@ -124,7 +124,7 @@ export function openSettings(tabText: string) {
export function openEmojiBoard(
cords,
requestEmojiCallback: (emoji: EmojiData) => void,
requestEmojiCallback: (_emoji: EmojiData) => void,
allowTextReactions: boolean,
) {
appDispatcher.dispatch({

View file

@ -1,29 +1,30 @@
/* eslint-disable no-restricted-globals */
/* eslint-disable no-useless-return */
import { openSearch, toggleRoomSettings } from '../action/navigation';
import navigation from '../state/navigation';
import { markAsRead } from '../action/notifications';
function shouldFocusMessageField(code: string) {
// do not focus on F keys
if (/^F\d+$/.test(code)) return false;
let hotkeysInitialized = false;
function initHotkeys() {
if (hotkeysInitialized) removeHotkeys();
hotkeysInitialized = true;
// do not focus on numlock/scroll lock
if (
code.startsWith('OS') ||
code.startsWith('Meta') ||
code.startsWith('Shift') ||
code.startsWith('Alt') ||
code.startsWith('Control') ||
code.startsWith('Arrow') ||
code === 'Tab' ||
code === 'Space' ||
code === 'Enter' ||
code === 'NumLock' ||
code === 'ScrollLock'
) {
return false;
}
document.body.addEventListener('keydown', listenKeyboard);
return true;
// detect browser forward and back
window.addEventListener('popstate', listenPopState);
history.pushState({ back: true }, '', '');
history.pushState({ present: true }, '', '');
history.pushState({ forward: true }, '', '');
history.back();
}
function removeHotkeys() {
document.body.removeEventListener('keydown', listenKeyboard);
window.removeEventListener('popstate', listenPopState);
hotkeysInitialized = false;
}
function listenKeyboard(e: KeyboardEvent) {
@ -139,69 +140,87 @@ function listenKeyboard(e: KeyboardEvent) {
}
}
/* eslint-disable no-restricted-globals */
/* eslint-disable no-useless-return */
function shouldFocusMessageField(code: string) {
// do not focus on F keys
if (/^F\d+$/.test(code)) return false;
// do not focus on numlock/scroll lock
if (
code.startsWith('OS') ||
code.startsWith('Meta') ||
code.startsWith('Shift') ||
code.startsWith('Alt') ||
code.startsWith('Control') ||
code.startsWith('Arrow') ||
code === 'Tab' ||
code === 'Space' ||
code === 'Enter' ||
code === 'NumLock' ||
code === 'ScrollLock'
) {
return false;
}
return true;
}
function listenPopState(e: PopStateEvent) {
console.log('popstate', e.state);
if (e.state.forward === true) {
if (e.state.forward) {
history.go(-1);
// open members list room-header__members-btn
const membersBtnEl = document.querySelector(
'.room-header__members-btn',
) as HTMLDivElement | null;
if (membersBtnEl && membersBtnEl.checkVisibility()) {
membersBtnEl.click();
return;
}
// open the current room room-selector--selected
const selectedRoomEl = document.querySelector(
'.room-selector--selected .room-selector__content',
) as HTMLDivElement | null;
if (selectedRoomEl && selectedRoomEl.checkVisibility()) {
selectedRoomEl.click();
return;
}
} else if (e.state.back === true) {
handleBrowserForward();
} else if (e.state.back) {
history.go(1);
// close room settings if open
const roomViewDroppedEl = document.querySelector(
'.room-settings__header-btn',
) as HTMLDivElement | null;
if (roomViewDroppedEl && roomViewDroppedEl.checkVisibility()) {
roomViewDroppedEl.click();
return;
}
// open the drawer if the button is visible
const roomHeaderBackBtnEl = document.querySelector(
'.room-header__back-btn',
) as HTMLDivElement | null;
if (roomHeaderBackBtnEl && roomHeaderBackBtnEl.checkVisibility()) {
roomHeaderBackBtnEl.click();
return;
}
handleBrowserBack();
}
}
function initHotkeys() {
document.body.addEventListener('keydown', listenKeyboard);
function handleBrowserForward() {
// open members list room-header__members-btn
const membersBtnEl = document.querySelector('.room-header__members-btn') as HTMLDivElement | null;
if (membersBtnEl && membersBtnEl.checkVisibility()) {
membersBtnEl.click();
return;
}
// detect browser forward and back
window.addEventListener('popstate', listenPopState);
history.pushState({ back: true }, '', '');
history.pushState({ present: true }, '', '');
history.pushState({ forward: true }, '', '');
history.back();
// open the current room room-selector--selected
const selectedRoomEl = document.querySelector(
'.room-selector--selected .room-selector__content',
) as HTMLDivElement | null;
const roomWrapperEl = document.querySelector('.room__wrapper') as HTMLDivElement | null;
if (
selectedRoomEl &&
selectedRoomEl.checkVisibility() &&
(!roomWrapperEl || !roomWrapperEl.checkVisibility())
) {
selectedRoomEl.click();
return;
}
navigation._navigateForward();
}
function removeHotkeys() {
document.body.removeEventListener('keydown', listenKeyboard);
window.removeEventListener('popstate', listenPopState);
function handleBrowserBack() {
// close room settings if open
const roomViewDroppedEl = document.querySelector(
'.room-settings__header-btn',
) as HTMLDivElement | null;
if (roomViewDroppedEl && roomViewDroppedEl.checkVisibility()) {
roomViewDroppedEl.click();
return;
}
// open the drawer if the button is visible
const roomHeaderBackBtnEl = document.querySelector(
'.room-header__back-btn',
) as HTMLDivElement | null;
if (roomHeaderBackBtnEl && roomHeaderBackBtnEl.checkVisibility()) {
roomHeaderBackBtnEl.click();
return;
}
navigation._navigateBack();
}
export { initHotkeys, removeHotkeys };

View file

@ -5,9 +5,10 @@ const cons = {
DEVICE_ID: 'cinny_device_id',
USER_ID: 'cinny_user_id',
BASE_URL: 'cinny_hs_base_url',
SLIDING_SYNC_PROXY_URL: 'cinny_sliding_sync_proxy_url',
},
DEVICE_DISPLAY_NAME: 'Variance Web',
ACTIVE_ROOM_ID: 'variance_active_room_id',
DEVICE_DISPLAY_NAME: 'Variance',
IN_CINNY_SPACES: 'in.cinny.spaces',
tabs: {
HOME: 'home',

View file

@ -20,6 +20,10 @@ class Navigation extends EventEmitter {
recentRooms: string[];
roomNavigationHistory: string[];
roomNavigationHistoryIndex: number;
spaceToRoom: Map<string, { roomId: string; timestamp: number }>;
rawModelStack: boolean[];
@ -37,6 +41,8 @@ class Navigation extends EventEmitter {
this.selectedThreadId = null;
this.isRoomSettings = false;
this.recentRooms = [];
this.roomNavigationHistory = [];
this.roomNavigationHistoryIndex = -1;
this.spaceToRoom = new Map();
@ -62,6 +68,7 @@ class Navigation extends EventEmitter {
_mapRoomToSpace(roomId: string) {
const { roomList } = this.initMatrix;
if (!roomList) return;
if (
this.selectedTab === cons.tabs.HOME &&
roomList.rooms.has(roomId) &&
@ -107,6 +114,44 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.ROOM_SETTINGS_TOGGLED, this.isRoomSettings);
}
// add to localStorage so we can restore it on reload
localStorage.setItem(cons.ACTIVE_ROOM_ID, roomId);
console.log('[select room] emitting ROOM_SELECTED');
// this gets accessed by hotkeys
console.log(
'roomNavigationHistory before',
this.roomNavigationHistoryIndex,
this.roomNavigationHistory,
);
if (this.roomNavigationHistoryIndex !== this.roomNavigationHistory.length - 1) {
// this means we previously went back
if (this.roomNavigationHistory[this.roomNavigationHistoryIndex] === roomId) {
// this means we went forwards/backwards normally. do nothing
} else {
// we selected a different room, delete history and add new room
this.roomNavigationHistory.splice(this.roomNavigationHistoryIndex);
this.roomNavigationHistory.push(roomId);
this.roomNavigationHistoryIndex += 1;
}
} else {
const lastRoomId = this.roomNavigationHistory[this.roomNavigationHistoryIndex];
// don't add to history if it's the same room
if (lastRoomId !== roomId) {
this.roomNavigationHistory.push(roomId);
this.roomNavigationHistoryIndex += 1;
}
}
console.log(
'roomNavigationHistory after',
this.roomNavigationHistoryIndex,
this.roomNavigationHistory,
);
this.emit(
cons.events.navigation.ROOM_SELECTED,
this.selectedRoomId,
@ -116,8 +161,26 @@ class Navigation extends EventEmitter {
);
}
_navigateBack() {
if (this.roomNavigationHistoryIndex <= 0) return;
this.roomNavigationHistoryIndex -= 1;
const roomId = this.roomNavigationHistory[this.roomNavigationHistoryIndex];
this._selectTabWithRoom(roomId);
this._selectRoom(roomId);
}
_navigateForward() {
console.log('_navigateForward', this.roomNavigationHistoryIndex);
if (this.roomNavigationHistoryIndex >= this.roomNavigationHistory.length - 1) return;
this.roomNavigationHistoryIndex += 1;
const roomId = this.roomNavigationHistory[this.roomNavigationHistoryIndex];
this._selectTabWithRoom(roomId);
this._selectRoom(roomId);
}
_selectTabWithRoom(roomId: string) {
const { roomList, accountData } = this.initMatrix;
if (!accountData) return;
const { categorizedSpaces } = accountData;
if (roomList.isOrphan(roomId)) {
@ -146,6 +209,7 @@ class Navigation extends EventEmitter {
}
const spaceInPath = [...this.selectedSpacePath].reverse().find((sId) => parents.has(sId));
console.log('[select room] spaceInPath', spaceInPath);
if (spaceInPath) {
this._selectSpace(spaceInPath, false, false);
return;
@ -251,8 +315,10 @@ class Navigation extends EventEmitter {
this._selectRoom(this._getLatestActiveRoomId(children));
}
_selectRoomWithTab(tabId) {
_selectRoomWithTab(tabId: string) {
const { roomList } = this.initMatrix;
if (!roomList) return;
if (tabId === cons.tabs.HOME || tabId === cons.tabs.DIRECTS) {
const data = this.spaceToRoom.get(tabId);
if (data) {
@ -266,7 +332,7 @@ class Navigation extends EventEmitter {
this._selectRoomWithSpace(tabId);
}
removeRecentRoom(roomId) {
removeRecentRoom(roomId: string) {
if (typeof roomId !== 'string') return;
const roomIdIndex = this.recentRooms.indexOf(roomId);
if (roomIdIndex >= 0) {
@ -274,7 +340,7 @@ class Navigation extends EventEmitter {
}
}
addRecentRoom(roomId) {
addRecentRoom(roomId: string) {
if (typeof roomId !== 'string') return;
this.recentRooms.push(roomId);