feat: explore page space banners, icon-derived gradients, and space descriptions
- Redesign explore cards with banner images, overlapping icons, and frosted fade - Extract dominant colors from space icons for dynamic banner gradients - Add space description/banner fields to schema with migration - Move origin label from banner overlay to content metadata row - Support space descriptions in settings overview panel
This commit is contained in:
@@ -52,7 +52,7 @@ interface ChatState {
|
||||
clearTyping: (channelId: string, userId: string) => void;
|
||||
getMessages: (channelId: string) => MessageWithUser[];
|
||||
getTypingUsers: (channelId: string) => TypingUser[];
|
||||
setReadStates: (readStates: ReadState[], channelLastMessageIds: Map<string, string>) => void;
|
||||
setReadStates: (readStates: ReadState[], channelLastMessageIds: Map<string, string>, originChannelIds?: Set<string>) => void;
|
||||
markChannelUnread: (channelId: string) => void;
|
||||
ackChannel: (channelId: string) => void;
|
||||
onChannelAck: (channelId: string, messageId: string) => void;
|
||||
@@ -473,13 +473,39 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
return users.filter(t => now - t.timestamp < 5000);
|
||||
},
|
||||
|
||||
setReadStates: (readStates: ReadState[], channelLastMessageIds: Map<string, string>) => {
|
||||
const rsMap = new Map<string, string>();
|
||||
setReadStates: (readStates: ReadState[], channelLastMessageIds: Map<string, string>, originChannelIds?: Set<string>) => {
|
||||
// 1. Merge server read states into existing local state (preserves optimistic acks)
|
||||
const rsMap = new Map(get().readStates);
|
||||
for (const rs of readStates) {
|
||||
rsMap.set(rs.channelId, rs.lastReadMessageId);
|
||||
const local = rsMap.get(rs.channelId);
|
||||
if (!local) {
|
||||
rsMap.set(rs.channelId, rs.lastReadMessageId);
|
||||
} else {
|
||||
try {
|
||||
if (BigInt(rs.lastReadMessageId) > BigInt(local)) {
|
||||
rsMap.set(rs.channelId, rs.lastReadMessageId);
|
||||
}
|
||||
} catch {
|
||||
rsMap.set(rs.channelId, rs.lastReadMessageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Rebuild unreadChannels ONLY for channels from this origin
|
||||
// Keep existing unread entries from other origins untouched
|
||||
const currentChannelId = get().currentChannelId;
|
||||
const unread = new Set<string>();
|
||||
for (const [channelId, lastMsgId] of channelLastMessageIds) {
|
||||
for (const id of get().unreadChannels) {
|
||||
if (!originChannelIds || !originChannelIds.has(id)) {
|
||||
unread.add(id); // preserve other-origin unreads
|
||||
}
|
||||
}
|
||||
|
||||
const channelsToCheck = originChannelIds ?? new Set(channelLastMessageIds.keys());
|
||||
for (const channelId of channelsToCheck) {
|
||||
if (channelId === currentChannelId) continue; // skip current channel (acked momentarily)
|
||||
const lastMsgId = channelLastMessageIds.get(channelId);
|
||||
if (!lastMsgId) continue; // empty channel
|
||||
const lastRead = rsMap.get(channelId);
|
||||
if (!lastRead) {
|
||||
unread.add(channelId);
|
||||
@@ -490,10 +516,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
unread.add(channelId);
|
||||
}
|
||||
} catch {
|
||||
// Corrupted read state (e.g. temp_ ID) — treat as unread
|
||||
unread.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
set({ readStates: rsMap, unreadChannels: unread });
|
||||
},
|
||||
|
||||
|
||||
@@ -111,6 +111,9 @@ export const useExploreStore = create<ExploreState>((set, get) => ({
|
||||
if (origin && space.icon) {
|
||||
space.icon = resolveAssetUrl(space.icon, origin) ?? space.icon;
|
||||
}
|
||||
if (origin && space.banner) {
|
||||
space.banner = resolveAssetUrl(space.banner, origin) ?? space.banner;
|
||||
}
|
||||
allSpaces.push({ ...space, _instanceOrigin: origin, joined: space.joined ?? false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,10 +203,13 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
const origin = space?._instanceOrigin ?? '';
|
||||
const client = getApiForOrigin(origin);
|
||||
const updated = await client.spaces.update(spaceId, data);
|
||||
// Normalize remote asset URL so the icon displays correctly in-app
|
||||
// Normalize remote asset URLs so the icon/banner display correctly in-app
|
||||
if (origin && updated.icon) {
|
||||
updated.icon = resolveAssetUrl(updated.icon, origin) ?? updated.icon;
|
||||
}
|
||||
if (origin && updated.banner) {
|
||||
updated.banner = resolveAssetUrl(updated.banner, origin) ?? updated.banner;
|
||||
}
|
||||
set((state) => ({
|
||||
spaces: state.spaces.map(s => s.id === spaceId ? { ...s, ...updated } : s),
|
||||
}));
|
||||
@@ -321,6 +324,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
icon: s.icon,
|
||||
banner: s.banner ?? null,
|
||||
ownerId: s.ownerId,
|
||||
inviteCode: s.inviteCode,
|
||||
visibility: s.visibility ?? 'private' as const,
|
||||
@@ -435,6 +439,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
|
||||
id: space.id,
|
||||
name: space.name,
|
||||
icon: space.icon,
|
||||
banner: space.banner ?? null,
|
||||
ownerId: space.ownerId,
|
||||
inviteCode: space.inviteCode,
|
||||
visibility: space.visibility,
|
||||
|
||||
Reference in New Issue
Block a user