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
360 changes: 64 additions & 296 deletions examples/vite/src/App.tsx

Large diffs are not rendered by default.

129 changes: 127 additions & 2 deletions examples/vite/src/AppSettings/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,60 @@ export type ThemeSettingsState = {
mode: 'dark' | 'light';
};

export const LEFT_PANEL_MIN_WIDTH = 360;
export const THREAD_PANEL_MIN_WIDTH = 360;

export type LeftPanelLayoutSettingsState = {
collapsed: boolean;
previousWidth: number;
width: number;
};

export type ThreadPanelLayoutSettingsState = {
width: number;
};

export type PanelLayoutSettingsState = {
leftPanel: LeftPanelLayoutSettingsState;
threadPanel: ThreadPanelLayoutSettingsState;
};

export type AppSettingsState = {
chatView: ChatViewSettingsState;
panelLayout: PanelLayoutSettingsState;
reactions: ReactionsSettingsState;
theme: ThemeSettingsState;
};

const panelLayoutStorageKey = 'stream-chat-react:example-panel-layout';
const themeStorageKey = 'stream-chat-react:example-theme-mode';
const themeUrlParam = 'theme';

const clamp = (value: number, min: number, max?: number) => {
const minClampedValue = Math.max(min, value);

if (typeof max !== 'number') return minClampedValue;

return Math.min(max, minClampedValue);
};

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null;

const defaultAppSettingsState: AppSettingsState = {
chatView: {
iconOnly: true,
},
panelLayout: {
leftPanel: {
collapsed: false,
previousWidth: LEFT_PANEL_MIN_WIDTH,
width: LEFT_PANEL_MIN_WIDTH,
},
threadPanel: {
width: THREAD_PANEL_MIN_WIDTH,
},
},
reactions: {
flipHorizontalPosition: false,
verticalPosition: 'top',
Expand All @@ -54,6 +95,59 @@ const getStoredThemeMode = (): ThemeSettingsState['mode'] | undefined => {
}
};

const normalizePanelLayoutSettings = (
value: unknown,
): PanelLayoutSettingsState | undefined => {
if (!isRecord(value)) return;

const leftPanel = isRecord(value.leftPanel) ? value.leftPanel : undefined;
const threadPanel = isRecord(value.threadPanel) ? value.threadPanel : undefined;

const leftPanelWidth = clamp(
typeof leftPanel?.width === 'number'
? leftPanel.width
: defaultAppSettingsState.panelLayout.leftPanel.width,
LEFT_PANEL_MIN_WIDTH,
);
const leftPanelPreviousWidth = clamp(
typeof leftPanel?.previousWidth === 'number'
? leftPanel.previousWidth
: leftPanelWidth,
LEFT_PANEL_MIN_WIDTH,
);
const threadPanelWidth = clamp(
typeof threadPanel?.width === 'number'
? threadPanel.width
: defaultAppSettingsState.panelLayout.threadPanel.width,
THREAD_PANEL_MIN_WIDTH,
);

return {
leftPanel: {
collapsed: leftPanel?.collapsed === true,
previousWidth: leftPanelPreviousWidth,
width: leftPanelWidth,
},
threadPanel: {
width: threadPanelWidth,
},
};
};

const getStoredPanelLayoutSettings = (): PanelLayoutSettingsState | undefined => {
if (typeof window === 'undefined') return;

try {
const storedPanelLayout = window.localStorage.getItem(panelLayoutStorageKey);

if (!storedPanelLayout) return;

return normalizePanelLayoutSettings(JSON.parse(storedPanelLayout));
} catch {
return;
}
};

const getThemeModeFromUrl = (): ThemeSettingsState['mode'] | undefined => {
if (typeof window === 'undefined') return;

Expand All @@ -74,6 +168,16 @@ const persistThemeMode = (themeMode: ThemeSettingsState['mode']) => {
}
};

const persistPanelLayoutSettings = (panelLayout: PanelLayoutSettingsState) => {
if (typeof window === 'undefined') return;

try {
window.localStorage.setItem(panelLayoutStorageKey, JSON.stringify(panelLayout));
} catch {
// ignore persistence failures in environments where localStorage is unavailable
}
};

const persistThemeModeInUrl = (themeMode: ThemeSettingsState['mode']) => {
if (typeof window === 'undefined') return;

Expand All @@ -92,6 +196,7 @@ const persistThemeModeInUrl = (themeMode: ThemeSettingsState['mode']) => {

const initialAppSettingsState: AppSettingsState = {
...defaultAppSettingsState,
panelLayout: getStoredPanelLayoutSettings() ?? defaultAppSettingsState.panelLayout,
theme: {
...defaultAppSettingsState.theme,
mode:
Expand All @@ -109,6 +214,26 @@ appSettingsStore.subscribeWithSelector(
},
);

appSettingsStore.subscribeWithSelector(
({ panelLayout }) => panelLayout,
(panelLayout) => {
persistPanelLayoutSettings(panelLayout);
},
);

export const updatePanelLayoutSettings = (
updater: (panelLayout: PanelLayoutSettingsState) => PanelLayoutSettingsState,
) => {
appSettingsStore.partialNext({
panelLayout: updater(appSettingsStore.getLatestValue().panelLayout),
});
};

export const useAppSettingsSelector = <
T extends Readonly<Record<string, unknown> | Readonly<unknown[]>>,
>(
selector: (state: AppSettingsState) => T,
): T => useStateStore(appSettingsStore, selector) ?? selector(initialAppSettingsState);

export const useAppSettingsState = () =>
useStateStore(appSettingsStore, (nextValue: AppSettingsState) => nextValue) ??
initialAppSettingsState;
useAppSettingsSelector((nextValue: AppSettingsState) => nextValue);
127 changes: 127 additions & 0 deletions examples/vite/src/ChatLayout/Panels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import clsx from 'clsx';
import type { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import { useRef } from 'react';
import {
AIStateIndicator,
Channel,
ChannelAvatar,
ChannelHeader,
ChannelList,
ChatView,
MessageInput,
MessageList,
Thread,
ThreadList,
TypingIndicator,
Window,
WithComponents,
WithDragAndDropUpload,
useChannelStateContext,
useChatContext,
} from 'stream-chat-react';
import { Search } from 'stream-chat-react/experimental';

import { SidebarResizeHandle, ThreadResizeHandle } from './Resize.tsx';
import { ThreadStateSync } from './Sync.tsx';

const ChannelThreadPanel = () => {
const { thread } = useChannelStateContext('ChannelThreadPanel');
const isOpen = !!thread;

return (
<>
<ThreadResizeHandle isOpen={isOpen} />
<WithDragAndDropUpload
className={clsx('str-chat__dropzone-root--thread app-chat-thread-panel', {
'app-chat-thread-panel--open': isOpen,
})}
>
<Thread virtualized />
</WithDragAndDropUpload>
</>
);
};

export const ChannelsPanels = ({
filters,
initialChannelId,
options,
sort,
}: {
filters: ChannelFilters;
initialChannelId?: string;
options: ChannelOptions;
sort: ChannelSort;
}) => {
const { navOpen = true } = useChatContext('ChannelsPanels');
const channelsLayoutRef = useRef<HTMLDivElement | null>(null);

return (
<ChatView.Channels>
<div
className={clsx('app-chat-view__channels-layout', {
'app-chat-view__channels-layout--sidebar-collapsed': navOpen === false,
})}
ref={channelsLayoutRef}
>
<ChannelList
ChannelSearch={Search}
Avatar={ChannelAvatar}
customActiveChannel={initialChannelId}
filters={filters}
options={options}
sort={sort}
showChannelSearch
/>
<SidebarResizeHandle layoutRef={channelsLayoutRef} />
<WithComponents overrides={{ TypingIndicator }}>
<Channel>
<WithDragAndDropUpload>
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput
focus
audioRecordingEnabled
maxRows={10}
asyncMessagesMultiSendEnabled
/>
</Window>
</WithDragAndDropUpload>
<ChannelThreadPanel />
</Channel>
</WithComponents>
</div>
</ChatView.Channels>
);
};

export const ThreadsPanels = () => {
const { navOpen = true } = useChatContext('ThreadsPanels');
const threadsLayoutRef = useRef<HTMLDivElement | null>(null);

return (
<ChatView.Threads>
<ThreadStateSync />
<div
className={clsx('app-chat-view__threads-layout', {
'app-chat-view__threads-layout--sidebar-collapsed': navOpen === false,
})}
ref={threadsLayoutRef}
>
<ThreadList />
<SidebarResizeHandle layoutRef={threadsLayoutRef} />
<div className='app-chat-view__threads-main'>
<ChatView.ThreadAdapter>
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
<WithComponents overrides={{ TypingIndicator }}>
<Thread virtualized />
</WithComponents>
</WithDragAndDropUpload>
</ChatView.ThreadAdapter>
</div>
</div>
</ChatView.Threads>
);
};
Loading
Loading