Skip to content

Commit

Permalink
Merge pull request #185 from jaczhi/jaczhi/chat-hide
Browse files Browse the repository at this point in the history
Use local storage to hide chat channels in UI
  • Loading branch information
zjkmxy authored Feb 26, 2025
2 parents cf256c2 + 973b02c commit 111f745
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 27 deletions.
14 changes: 14 additions & 0 deletions src/components/chat/chat-state-store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createSignal } from "solid-js";

Check failure on line 1 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L1

Replace `"solid-js";` with `'solid-js'` (prettier/prettier)

export function useChatState<T>(keyName: string, defaultValue: T) {
const [chatState, setChatState] = createSignal<T>(
JSON.parse(localStorage.getItem(keyName) || JSON.stringify(defaultValue))

Check failure on line 5 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L5

Insert `,` (prettier/prettier)
);

Check failure on line 6 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L6

Delete `;` (prettier/prettier)

const updateChatState = (newValue: T) => {
setChatState(() => newValue);

Check failure on line 9 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L9

Delete `;` (prettier/prettier)
localStorage.setItem(keyName, JSON.stringify(newValue));

Check failure on line 10 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L10

Delete `;` (prettier/prettier)
};

Check failure on line 11 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L11

Delete `;` (prettier/prettier)

return { chatState, updateChatState };

Check failure on line 13 in src/components/chat/chat-state-store.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat-state-store.tsx#L13

Delete `;` (prettier/prettier)
}
116 changes: 89 additions & 27 deletions src/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { batch, createEffect, createMemo, createSignal, For, on } from 'solid-js'
import {batch, createEffect, createMemo, createSignal, For, on} from 'solid-js'

Check failure on line 1 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L1

Replace `batch,·createEffect,·createMemo,·createSignal,·For,·on` with `·batch,·createEffect,·createMemo,·createSignal,·For,·on·` (prettier/prettier)
import { boxed } from '@syncedstore/core'
import { useNdnWorkspace } from '../../Context'
import { chats } from '../../backend/models'
import { createSyncedStoreSig } from '../../adaptors/solid-synced-store'
import { AddChannelDialog } from './add-channel-dialog.tsx'
import { useChatState } from "./chat-state-store.tsx";

Check failure on line 6 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L6

Replace `"./chat-state-store.tsx";` with `'./chat-state-store.tsx'` (prettier/prettier)
import { AddChannelDialog } from "./add-channel-dialog.tsx";

Check failure on line 7 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L7

Replace `"./add-channel-dialog.tsx";` with `'./add-channel-dialog.tsx'` (prettier/prettier)
import { ToggleChannelVisibilityDialog } from "./toggle-visibility-dialog.tsx";

Check failure on line 8 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L8

Replace `"./toggle-visibility-dialog.tsx";` with `'./toggle-visibility-dialog.tsx'` (prettier/prettier)
import styles from './styles.module.scss'
import { useNavigate } from '@solidjs/router'
import { SolidMarkdown, SolidMarkdownComponents } from 'solid-markdown'
Expand All @@ -17,28 +19,38 @@ export function Chat() {
const messages = createSyncedStoreSig(() => rootDoc()?.chats)
const data = () => messages()?.value
const username = () => syncAgent()?.nodeId.at(-1).text ?? ''
const { chatState: hiddenChannels, updateChatState: setHiddenChannels } = useChatState<string[]>(`${syncAgent()?.nodeId}/hiddenChannels`, [])

Check failure on line 22 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L22

Replace ``${syncAgent()?.nodeId}/hiddenChannels`,·[]` with `⏎····`${syncAgent()?.nodeId}/hiddenChannels`,⏎····[],⏎··` (prettier/prettier)

const [messageTerm, setMessageTerm] = createSignal('')
const [container, setContainer] = createSignal<HTMLDivElement>()
const [currentChannel, setCurrentChannel] = createSignal('general')
const [isAddChannelDialogOpen, setIsAddChannelDialogOpen] = createSignal(false)
const [isToggleVisibilityDialogOpen, setIsToggleVisibilityDialogOpen] = createSignal(false)
const [persistedChannels, setPersistedChannels] = createSignal<string[]>(['general'])

const channels = createMemo(() => {
const messageData = data()
if (!messageData) return persistedChannels()
const channels = createMemo<string[]>((): string[] => {
const messageData = data();

Check failure on line 32 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L32

Delete `;` (prettier/prettier)
if (!messageData) {
return Array.from(new Set(persistedChannels()).difference(new Set(hiddenChannels())))
}

const uniqueChannels = new Set(persistedChannels())
messageData.forEach((msg) => {
const uniqueChannels: Set<string> = new Set();

Check failure on line 37 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L37

Delete `;` (prettier/prettier)
messageData.forEach(msg => {

Check failure on line 38 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L38

Replace `msg` with `(msg)` (prettier/prettier)
if (msg.value.channel) {
uniqueChannels.add(msg.value.channel)
uniqueChannels.add(msg.value.channel);

Check failure on line 40 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L40

Delete `;` (prettier/prettier)
}
})
});

Check failure on line 42 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L42

Delete `;` (prettier/prettier)

const finalChannels: Set<string> = uniqueChannels
.union(new Set(persistedChannels()))
.difference(new Set(hiddenChannels()))

return Array.from(uniqueChannels).sort()
})
return Array.from(finalChannels).sort();

Check failure on line 48 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L48

Delete `;` (prettier/prettier)
});

Check failure on line 49 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L49

Delete `;` (prettier/prettier)

const filteredMessages = createMemo(() => data()?.filter((msg) => msg.value.channel === currentChannel()))
const filteredMessages = createMemo(() =>

Check failure on line 51 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L51

Replace `⏎····data()?.filter((msg)·=>·msg.value.channel·===·currentChannel())⏎··);` with `·data()?.filter((msg)·=>·msg.value.channel·===·currentChannel()))` (prettier/prettier)
data()?.filter((msg) => msg.value.channel === currentChannel())
);

if (!booted()) {
navigate('/profile', { replace: true })
Expand Down Expand Up @@ -69,24 +81,42 @@ export function Chat() {
}),
)

createEffect(
on(channels, () => {
if (!channels().includes(currentChannel())) {
setCurrentChannel(channels()[0])
}
}),
)

const addChannel = (channelName: string) => {
const trimmedName = channelName.trim()
if (trimmedName && !channels().includes(trimmedName)) {
if (trimmedName && !channels().includes(trimmedName) && !hiddenChannels().includes(trimmedName)) {
setPersistedChannels([...persistedChannels(), trimmedName])
setCurrentChannel(trimmedName)
} else {
}

Check failure on line 97 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L97

Delete `⏎···` (prettier/prettier)
else {
alert('Channel name cannot be empty or already exist')
}
}

const hideChannel = (channelName: string) => {
if (channels().length === 1) {
alert('Cannot hide the last channel')
return
}

setHiddenChannels([...new Set([...hiddenChannels(), channelName])])
}

const isLocalUser = (sender: string) => sender == username()

const userPfpId = (sender: string) => {
let hash = 0
let hash = 0;

Check failure on line 115 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L115

Delete `;` (prettier/prettier)
for (let i = 0; i < sender.length; i++) {
hash = (hash * 31 + sender.charCodeAt(i)) >>> 0 // Ensure the hash is always a 32-bit unsigned integer
hash = (hash * 31 + sender.charCodeAt(i)) >>> 0; // Ensure the hash is always a 32-bit unsigned integer

Check failure on line 117 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L117

Delete `;·` (prettier/prettier)
}
return hash % 1024
return hash % 1024;

Check failure on line 119 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L119

Delete `;` (prettier/prettier)
}

/* Display */
Expand All @@ -109,12 +139,32 @@ export function Chat() {
onClick={() => setCurrentChannel(channel)}
>
#{channel}
<span
title={"Hide channel"}

Check failure on line 143 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L143

Replace `"Hide·channel"` with `'Hide·channel'` (prettier/prettier)
class={styles.HideChannelButton}
aria-label={"Hide channel"}

Check failure on line 145 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L145

Replace `"Hide·channel"` with `'Hide·channel'` (prettier/prettier)
onClick={(event) => {
event.stopPropagation()
hideChannel(channel)
}

Check failure on line 149 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L149

Insert `}` (prettier/prettier)
}>x</span>

Check failure on line 150 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L150

Replace `}>x` with `>⏎··················x⏎················` (prettier/prettier)
</button>
)}
</For>
<button class={styles.AddChannelButton} onClick={() => setIsAddChannelDialogOpen(true)}>
<button
class={styles.AddChannelButton}
onClick={() => setIsAddChannelDialogOpen(true)}
title={"New channel"}

Check failure on line 157 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L157

Replace `"New·channel"` with `'New·channel'` (prettier/prettier)
aria-label={"New channel"}>

Check failure on line 158 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L158

Replace `"New·channel"}` with `'New·channel'}⏎··········` (prettier/prettier)
+
</button>
<button
class={styles.AddChannelButton}
onClick={() => setIsToggleVisibilityDialogOpen(true)}
title={"Unhide channels"}

Check failure on line 164 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L164

Replace `"Unhide·channels"` with `'Unhide·channels'` (prettier/prettier)
aria-label={"Un-hide channels"}>

Check failure on line 165 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L165

Replace `"Un-hide·channels"}` with `'Un-hide·channels'}⏎··········` (prettier/prettier)
&#x2630;
</button>
</div>
<h2 class={styles.ChannelHeading}>#{currentChannel()} Channel</h2>
</div>
Expand All @@ -123,21 +173,25 @@ export function Chat() {
{(msg) => (
<div>
<div class={styles.App__message}>
<img src={`https://picsum.photos/id/${userPfpId(msg.value.sender)}/128/128`} style="flex-shrink: 0" />
<img
aria-hidden={true}
alt={`${msg.value.sender}'s profile picture`}
src={`https://picsum.photos/id/${userPfpId(msg.value.sender)}/128/128`}
style="flex-shrink: 0"
/>
<div class={styles.App__msgContent}>
<h4
class={`${styles.App__msgHeader}
${isLocalUser(msg.value.sender) ? styles.App__backgroundLocal : styles.App__backgroundForeign}
${isLocalUser(msg.value.sender) ? styles.App__borderLocal : styles.App__borderForeign}`}
>
{' '}
{msg.value.sender}{' '}
<span>
{new Date(msg.value.timestamp).toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
})}
</span>
{msg.value.sender}

Check failure on line 189 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L189

Delete `⏎····················` (prettier/prettier)
{' '}
<span>{new Date(msg.value.timestamp).toLocaleString(undefined, {

Check failure on line 191 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L191

Insert `⏎······················` (prettier/prettier)
dateStyle: 'medium',

Check failure on line 192 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L192

Insert `··` (prettier/prettier)
timeStyle: 'short'

Check failure on line 193 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L193

Replace `timeStyle:·'short'` with `··timeStyle:·'short',` (prettier/prettier)
})}</span>

Check failure on line 194 in src/components/chat/chat.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/chat.tsx#L194

Replace `····················})}` with `······················})}⏎····················` (prettier/prettier)
</h4>
<div
class={`${styles.App_msgContentSolid}
Expand All @@ -158,7 +212,7 @@ export function Chat() {
<div class={styles.App__input}>
<textarea
name="message"
placeholder={`Message the ${currentChannel()} channel`}
placeholder={`Message the #${currentChannel()} channel`}
onChange={(event) => setMessageTerm(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
Expand All @@ -184,6 +238,14 @@ export function Chat() {
setIsAddChannelDialogOpen(false)
}}
/>

<ToggleChannelVisibilityDialog
open={isToggleVisibilityDialogOpen()}
channels={channels}
hiddenChannels={hiddenChannels}
setHiddenChannels={setHiddenChannels}
onClose={() => setIsToggleVisibilityDialogOpen(false)}
/>
</div>
)
}
43 changes: 43 additions & 0 deletions src/components/chat/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@
margin-right: 8px;
transition: background-color 0.3s;

position: relative;
display: inline-flex;
align-items: center;

&:hover {
background-color: #f4f4f4;
}
Expand All @@ -216,6 +220,10 @@
margin-bottom: 8px;
transition: background-color 0.3s;

position: relative;
display: inline-flex;
align-items: center;

&:hover {
background-color: #0056b3;
}
Expand Down Expand Up @@ -246,3 +254,38 @@
margin: 0;
padding-left: 12px;
}

.HideChannelButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #e0e0e0;
color: #555;
font-size: 8px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
line-height: 1;
margin-left: 0.5rem;

vertical-align: middle;
margin-top: auto;
margin-bottom: auto;
}

.HideChannelButton:hover {
background-color: #f9f9f9;
}

.HideChannelButton:active {
transform: scale(0.95);
}

.HideChannelButton:focus {
outline: 2px solid #0096ff;
outline-offset: 2px;
}
93 changes: 93 additions & 0 deletions src/components/chat/toggle-visibility-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, List, ListItem, Checkbox, FormControlLabel } from '@suid/material'

Check failure on line 1 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L1

Replace `·Dialog,·DialogTitle,·DialogContent,·DialogActions,·Button,·List,·ListItem,·Checkbox,·FormControlLabel·` with `⏎··Dialog,⏎··DialogTitle,⏎··DialogContent,⏎··DialogActions,⏎··Button,⏎··List,⏎··ListItem,⏎··Checkbox,⏎··FormControlLabel,⏎` (prettier/prettier)
import { createSignal, createEffect, For } from 'solid-js'

interface ToggleChannelVisibilityDialogProps {
open: boolean
channels: () => string[] // visible channels

Check failure on line 6 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L6

Delete `·` (prettier/prettier)
hiddenChannels: () => string[] // hidden channels

Check failure on line 7 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L7

Delete `·` (prettier/prettier)
setHiddenChannels: (channels: string[]) => void
onClose: () => void
}

export function ToggleChannelVisibilityDialog(props: ToggleChannelVisibilityDialogProps) {
// Track all displayed channels and their checked state
const [selectedChannels, setSelectedChannels] = createSignal<string[]>([])

// Initialize selected channels when dialog opens
createEffect(() => {
if (props.open) {
// Start with currently visible channels
setSelectedChannels([...props.channels()])
}
})

const handleToggleChannel = (channel: string, checked: boolean) => {
if (checked) {
setSelectedChannels([...selectedChannels(), channel])
} else {
setSelectedChannels(selectedChannels().filter(c => c !== channel))

Check failure on line 28 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L28

Replace `c` with `(c)` (prettier/prettier)
}
}

const handleSave = () => {
if (selectedChannels().length === 0) {
// At least one channel must be visible
return
}

// Calculate which channels should be hidden (all channels minus selected ones)
const allChannels = [...props.channels(), ...props.hiddenChannels()]
const newHiddenChannels = allChannels.filter(

Check failure on line 40 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L40

Replace `⏎······channel·=>·!selectedChannels().includes(channel)⏎····` with `(channel)·=>·!selectedChannels().includes(channel)` (prettier/prettier)
channel => !selectedChannels().includes(channel)
)
props.setHiddenChannels(newHiddenChannels)
props.onClose()
}

const isChannelSelected = (channel: string) => {
return selectedChannels().includes(channel)
}

// Get all channels (visible and hidden)
const allChannels = () => {
const combined = [...props.channels(), ...props.hiddenChannels()]
return [...new Set(combined)].sort() // Remove duplicates and sort
}

return (
<Dialog

Check failure on line 58 in src/components/chat/toggle-visibility-dialog.tsx

View workflow job for this annotation

GitHub Actions / ESLint

src/components/chat/toggle-visibility-dialog.tsx#L58

Replace `⏎······open={props.open}⏎······onClose={props.onClose}⏎······fullWidth⏎······maxWidth="sm"⏎····` with `·open={props.open}·onClose={props.onClose}·fullWidth·maxWidth="sm"` (prettier/prettier)
open={props.open}
onClose={props.onClose}
fullWidth
maxWidth="sm"
>
<DialogTitle>Toggle Channel Visibility</DialogTitle>
<DialogContent>
<List sx={{ minWidth: '300px' }}>
<For each={allChannels()}>
{(channel) => (
<ListItem disablePadding>
<FormControlLabel
control={
<Checkbox
checked={isChannelSelected(channel)}
onChange={(_, checked) => handleToggleChannel(channel, checked)}
/>
}
label={`#${channel}`}
sx={{ width: '100%', margin: '4px 0' }}
/>
</ListItem>
)}
</For>
</List>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose}>Cancel</Button>
<Button onClick={handleSave} disabled={selectedChannels().length === 0} color="primary">
Save
</Button>
</DialogActions>
</Dialog>
)
}

0 comments on commit 111f745

Please sign in to comment.