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
17 changes: 14 additions & 3 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ import { init, SearchIndex } from 'emoji-mart';
import data from '@emoji-mart/data/sets/14/native.json';
import { humanId } from 'human-id';
import { chatViewSelectorItemSet } from './Sidebar/ChatViewSelectorItemSet.tsx';
import { useAppSettingsState } from './AppSettings';

import { Search } from 'stream-chat-react/experimental';
import { useAppSettingsState } from './AppSettings/state.ts';

init({ data });

Expand Down Expand Up @@ -199,9 +199,17 @@ const CustomMessageReactions = (props: React.ComponentProps<typeof ReactionsList
);
};

const EmojiPickerWithCustomOptions = (
props: React.ComponentProps<typeof EmojiPicker>,
) => {
const state = useAppSettingsState();

return <EmojiPicker {...props} pickerProps={{ theme: state.theme.mode }} />;
};

const App = () => {
const { userId, tokenProvider } = useUser();
const { chatView } = useAppSettingsState();
const { chatView, theme } = useAppSettingsState();
const initialChannelId = useMemo(() => getSelectedChannelIdFromUrl(), []);
const initialChatView = useMemo(() => getSelectedChatViewFromUrl(), []);

Expand Down Expand Up @@ -288,11 +296,13 @@ const App = () => {

if (!chatClient) return <>Loading...</>;

const chatTheme = theme.mode === 'dark' ? 'str-chat__theme-dark' : 'messaging light';

return (
<WithComponents
overrides={{
emojiSearchIndex: SearchIndex,
EmojiPicker,
EmojiPicker: EmojiPickerWithCustomOptions,
ReactionsList: CustomMessageReactions,
reactionOptions: newReactionOptions,
}}
Expand All @@ -301,6 +311,7 @@ const App = () => {
searchController={searchController}
client={chatClient}
isMessageAIGenerated={isMessageAIGenerated}
theme={chatTheme}
>
<ChatView>
<ChatStateSync initialChatView={initialChatView} />
Expand Down
31 changes: 15 additions & 16 deletions examples/vite/src/AppSettings/AppSettings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@

.app__settings-group_button {
color: var(--text-secondary);

svg {
height: 2rem;
width: 2rem;
}
}
}

Expand All @@ -22,7 +17,9 @@
width: min(920px, 90vw);
max-height: min(80vh, 760px);
min-height: min(520px, 72vh);
background: #fff;
background: var(--background-elevation-elevation-2);
color: var(--text-primary);
border: 1px solid var(--border-core-default);
border-radius: 14px;
}

Expand All @@ -33,7 +30,7 @@
padding: 16px 20px;
font-size: 1.5rem;
font-weight: 700;
border-bottom: 1px solid #dfe5ef;
border-bottom: 1px solid var(--border-core-default);

svg.str-chat__icon--cog {
height: 1.75rem;
Expand All @@ -51,7 +48,7 @@
.app__settings-modal__tabs {
overflow-y: auto;
overscroll-behavior: contain;
border-right: 1px solid #dfe5ef;
border-right: 1px solid var(--border-core-default);
padding: 10px;
}

Expand All @@ -61,12 +58,14 @@
justify-content: flex-start;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-secondary);
}

.app__settings-modal__tab[aria-selected='true'],
.app__settings-modal__tab.app__settings-modal__tab--active {
background: #e9f0ff;
border-color: #3167f6;
background: var(--background-core-selected);
border-color: var(--border-utility-selected);
color: var(--text-primary);
font-weight: 600;
}

Expand All @@ -90,7 +89,7 @@

.app__settings-modal__field-label {
font-weight: 600;
color: #2f3550;
color: var(--text-primary);
}

.app__settings-modal__options-row {
Expand All @@ -100,8 +99,8 @@
}

.app__settings-modal__option-button[aria-pressed='true'] {
border-color: #3167f6;
background: #e9f0ff;
border-color: var(--border-utility-selected);
background: var(--background-core-selected);
font-weight: 600;
}

Expand All @@ -113,10 +112,10 @@
}

.app__settings-modal__preview {
border: 1px solid var(--border);
border: 1px solid var(--border-core-default);
border-radius: 12px;
padding: 12px;
background: var(--bg-surface);
background: var(--background-core-surface);

.str-chat__li--single {
list-style: none;
Expand All @@ -141,7 +140,7 @@

.app__settings-modal__tabs {
border-right: 0;
border-bottom: 1px solid #dfe5ef;
border-bottom: 1px solid var(--border-core-default);
display: flex;
gap: 8px;
padding: 10px 12px;
Expand Down
30 changes: 30 additions & 0 deletions examples/vite/src/AppSettings/AppSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
GlobalModal,
IconBubble3ChatMessage,
IconEmojiSmile,
IconLightBulbSimple,
IconSettingsGear2,
} from 'stream-chat-react';
import { type ComponentType, useState } from 'react';
import { ReactionsTab } from './tabs/Reactions';
import { SidebarTab } from './tabs/Sidebar';
import { appSettingsStore, useAppSettingsState } from './state';

type TabId = 'reactions' | 'sidebar';

Expand All @@ -17,13 +19,41 @@ const tabConfig: { Icon: ComponentType; id: TabId; title: string }[] = [
{ Icon: IconEmojiSmile, id: 'reactions', title: 'Reactions' },
];

const SidebarThemeToggle = ({ iconOnly = true }: { iconOnly?: boolean }) => {
const {
theme: { mode },
} = useAppSettingsState();
const nextMode = mode === 'dark' ? 'light' : 'dark';

return (
<ChatViewSelectorButton
aria-checked={mode === 'dark'}
aria-label={`Switch to ${nextMode} mode`}
aria-selected={mode === 'dark'}
className='app__settings-group_button'
iconOnly={iconOnly}
Icon={IconLightBulbSimple}
isActive={mode === 'dark'}
onClick={() =>
appSettingsStore.partialNext({
theme: { mode: nextMode },
})
}
role='switch'
text={mode === 'dark' ? 'Dark mode' : 'Light mode'}
/>
);
};

export const AppSettings = ({ iconOnly = true }: { iconOnly?: boolean }) => {
const [activeTab, setActiveTab] = useState<TabId>('sidebar');
const [open, setOpen] = useState(false);

return (
<div className='app__settings-group'>
<SidebarThemeToggle iconOnly={iconOnly} />
<ChatViewSelectorButton
className='app__settings-group_button'
iconOnly={iconOnly}
Icon={IconSettingsGear2}
onClick={() => setOpen(true)}
Expand Down
84 changes: 82 additions & 2 deletions examples/vite/src/AppSettings/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ export type ChatViewSettingsState = {
iconOnly: boolean;
};

export type ThemeSettingsState = {
mode: 'dark' | 'light';
};

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

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

const defaultAppSettingsState: AppSettingsState = {
chatView: {
iconOnly: true,
Expand All @@ -25,10 +33,82 @@ const defaultAppSettingsState: AppSettingsState = {
verticalPosition: 'top',
visualStyle: 'clustered',
},
theme: {
mode: 'light',
},
};

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

let storedThemeMode: string | null = null;

try {
storedThemeMode = window.localStorage.getItem(themeStorageKey);
} catch {
return;
}

if (storedThemeMode === 'dark' || storedThemeMode === 'light') {
return storedThemeMode;
}
};

export const appSettingsStore = new StateStore<AppSettingsState>(defaultAppSettingsState);
const getThemeModeFromUrl = (): ThemeSettingsState['mode'] | undefined => {
if (typeof window === 'undefined') return;

const themeMode = new URLSearchParams(window.location.search).get(themeUrlParam);

if (themeMode === 'dark' || themeMode === 'light') {
return themeMode;
}
};

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

try {
window.localStorage.setItem(themeStorageKey, themeMode);
} catch {
// ignore persistence failures in environments where localStorage is unavailable
}
};

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

const url = new URL(window.location.href);

if (url.searchParams.get(themeUrlParam) === themeMode) return;

url.searchParams.set(themeUrlParam, themeMode);

window.history.replaceState(
window.history.state,
'',
`${url.pathname}${url.search}${url.hash}`,
);
};

const initialAppSettingsState: AppSettingsState = {
...defaultAppSettingsState,
theme: {
...defaultAppSettingsState.theme,
mode:
getThemeModeFromUrl() ?? getStoredThemeMode() ?? defaultAppSettingsState.theme.mode,
},
};

export const appSettingsStore = new StateStore<AppSettingsState>(initialAppSettingsState);

appSettingsStore.subscribeWithSelector(
({ theme }) => ({ mode: theme.mode }),
({ mode }) => {
persistThemeMode(mode);
persistThemeModeInUrl(mode);
},
);

export const useAppSettingsState = () =>
useStateStore(appSettingsStore, (nextValue: AppSettingsState) => nextValue) ??
defaultAppSettingsState;
initialAppSettingsState;
7 changes: 4 additions & 3 deletions src/components/Attachment/styling/ModalGallery.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
--str-chat__modal-gallery-load-failed-indicator-background: var(--accent-error);
--str-chat__modal-gallery-load-failed-indicator-color: var(--text-inverse);
--str-chat__modal-gallery-loading-background: var(--chat-bg-incoming);
--str-chat__modal-gallery-loading-highlight: var(--base-white);
--str-chat__modal-gallery-loading-base: var(--skeleton-loading-base);
--str-chat__modal-gallery-loading-highlight: var(--skeleton-loading-highlight);
}

.str-chat__message--me {
Expand Down Expand Up @@ -140,9 +141,9 @@
background-color: var(--str-chat__modal-gallery-loading-background);
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
var(--str-chat__modal-gallery-loading-base) 0%,
var(--str-chat__modal-gallery-loading-highlight) 50%,
rgba(255, 255, 255, 0) 100%
var(--str-chat__modal-gallery-loading-base) 100%
);
background-repeat: no-repeat;
background-size: 200% 100%;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Channel/styling/Channel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@
/* The icon color used when no channel is selected */
--str-chat__channel-empty-indicator-color: var(--str-chat__disabled-color);

/* The color of the loading indicator */
--str-chat__channel-loading-state-color: var(--slate-100);
/* The base surface color behind the loading shimmer */
--str-chat__channel-loading-state-color: var(--background-core-surface);
}

.str-chat__channel {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Chat/__tests__/Chat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('Chat', () => {
});

it('props change should update the context', async () => {
const theme = 'team dark';
const theme = 'str-chat__theme-dark';
let context;
const { rerender } = render(
<Chat client={chatClient} theme={theme}>
Expand All @@ -86,7 +86,7 @@ describe('Chat', () => {
expect(context.theme).toBe(theme);
});

const newTheme = 'messaging dark';
const newTheme = 'str-chat__theme-dark custom-theme';
const newClient = getTestClient();
rerender(
<Chat client={newClient} theme={newTheme}>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const IconBookmark = createIcon(
<path
d='M12.8333 13.501V3.16666C12.8333 2.43028 12.2364 1.83333 11.5 1.83333H4.49999C3.76361 1.83333 3.16666 2.43028 3.16666 3.16666V13.501C3.16666 14.0348 3.76275 14.3521 4.20558 14.054L7.25546 12.0011C7.70559 11.6982 8.29439 11.6982 8.74452 12.0011L11.7944 14.054C12.2373 14.3521 12.8333 14.0348 12.8333 13.501Z'
fill='none'
stroke='black'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>,
Expand Down Expand Up @@ -364,14 +364,14 @@ export const IconCloseQuote2 = createIcon(
<path
d='M5.50001 3.16666H3.16668C2.4303 3.16666 1.83334 3.76361 1.83334 4.49999V7.35712C1.83334 8.09352 2.4303 8.69046 3.16668 8.69046H4.16668V12.8333C4.16668 12.8333 6.83334 11.7976 6.83334 8.69046V4.49907C6.83334 3.76269 6.23639 3.16666 5.50001 3.16666Z'
fill='none'
stroke='black'
stroke='currentColor'
strokeLinejoin='round'
strokeWidth='1.2'
/>
<path
d='M12.8333 3.16666H10.5C9.76361 3.16666 9.16668 3.76361 9.16668 4.49999V7.35712C9.16668 8.09352 9.76361 8.69046 10.5 8.69046H11.5V12.8333C11.5 12.8333 14.1667 11.7976 14.1667 8.69046V4.49907C14.1667 3.76269 13.5697 3.16666 12.8333 3.16666Z'
fill='none'
stroke='black'
stroke='currentColor'
strokeLinejoin='round'
strokeWidth='1.2'
/>
Expand Down
Loading
Loading