frontend for xcvr appview

maybe this will allow for reading media + message history combined

+80 -22
+18 -9
src/lib/components/History.svelte
··· 1 <script lang="ts"> 2 import MessageTransmission from "$lib/components/MessageTransmission.svelte"; 3 - import type { SignedMessageView } from "$lib/types"; 4 - import { calculateMarginTop, signedMessageViewToMessage } from "$lib/utils"; 5 interface Props { 6 - messages: Array<SignedMessageView>; 7 } 8 - let { messages }: Props = $props(); 9 </script> 10 11 <div id="receiver"> 12 - {#each [...messages].reverse() as message, i} 13 - <MessageTransmission 14 - message={signedMessageViewToMessage(message)} 15 - margin={0} 16 - /> 17 {/each} 18 </div> 19
··· 1 <script lang="ts"> 2 import MessageTransmission from "$lib/components/MessageTransmission.svelte"; 3 + import type { SignedItemView } from "$lib/types"; 4 + import { 5 + signedImageViewToImage, 6 + signedMessageViewToMessage, 7 + } from "$lib/utils"; 8 + import ImageTransmission from "./ImageTransmission.svelte"; 9 interface Props { 10 + items: Array<SignedItemView>; 11 } 12 + let { items }: Props = $props(); 13 </script> 14 15 <div id="receiver"> 16 + {#each [...items].reverse() as item, i (`${item.signet.lrcId}-${item.type}`)} 17 + {#if item.type === "message"} 18 + <MessageTransmission 19 + message={signedMessageViewToMessage(item)} 20 + margin={0} 21 + /> 22 + {/if} 23 + {#if item.type === "image"} 24 + <ImageTransmission image={signedImageViewToImage(item)} margin={0} /> 25 + {/if} 26 {/each} 27 </div> 28
+18
src/lib/types.ts
··· 161 nick?: string 162 color?: number 163 signetURI: string 164 } 165 166 export type ItemView = MessageView | MediaView ··· 172 aspectRatio?: AspectRatio 173 } 174 175 export type SignedMessageView = { 176 $type?: string 177 uri: string 178 author: ProfileView ··· 182 signet: SignetView 183 postedAt: string 184 }
··· 161 nick?: string 162 color?: number 163 signetURI: string 164 + postedAt: string 165 } 166 167 export type ItemView = MessageView | MediaView ··· 173 aspectRatio?: AspectRatio 174 } 175 176 + export type SignedItemView = SignedMessageView | SignedMediaView 177 + 178 export type SignedMessageView = { 179 + type: 'message' 180 $type?: string 181 uri: string 182 author: ProfileView ··· 186 signet: SignetView 187 postedAt: string 188 } 189 + 190 + export type SignedMediaView = SignedImageView 191 + 192 + export type SignedImageView = { 193 + type: 'image' 194 + $type?: string 195 + uri: string 196 + author: ProfileView 197 + image: ImageView 198 + nick?: string 199 + color?: number 200 + signet: SignetView 201 + postedAt: string 202 + }
+23 -1
src/lib/utils.ts
··· 1 import type { ChannelView } from "$lib/types.ts" 2 - import type { SignedMessageView, Message } from "$lib/types.ts" 3 export function getChannelWS(c: ChannelView): string | null { 4 const host = c.host 5 const uri = c.uri ··· 57 const elapsedMs = currentTime - previousTime; 58 const elapsedMinutes = elapsedMs / (1000 * 60); 59 return Math.log(elapsedMinutes + 1); 60 } 61 62 export function signedMessageViewToMessage(sm: SignedMessageView): Message {
··· 1 import type { ChannelView } from "$lib/types.ts" 2 + import type { SignedImageView, SignedMessageView, Message, Image } from "$lib/types.ts" 3 export function getChannelWS(c: ChannelView): string | null { 4 const host = c.host 5 const uri = c.uri ··· 57 const elapsedMs = currentTime - previousTime; 58 const elapsedMinutes = elapsedMs / (1000 * 60); 59 return Math.log(elapsedMinutes + 1); 60 + } 61 + 62 + export function signedImageViewToImage(sm: SignedImageView): Image { 63 + return { 64 + type: 'image', 65 + id: sm.signet.lrcId, 66 + lrcdata: { 67 + muted: false, 68 + mine: false 69 + }, 70 + signetView: sm.signet, 71 + mediaView: { 72 + $type: sm.$type, 73 + uri: sm.uri, 74 + author: sm.author, 75 + imageView: sm.image, 76 + ...(sm.nick && { nick: sm.nick }), 77 + ...(sm.color && { color: sm.color }), 78 + signetURI: sm.signet.uri, 79 + postedAt: sm.postedAt 80 + } 81 + } 82 } 83 84 export function signedMessageViewToMessage(sm: SignedMessageView): Message {
+17 -8
src/routes/c/[handle]/[rkey]/history/+page.svelte
··· 1 <script lang="ts"> 2 import type { PageProps } from "./$types"; 3 import History from "$lib/components/History.svelte"; 4 let { data }: PageProps = $props(); 5 - let messages = $state(data.messages); 6 let nextCursor = $state(data.cursor); 7 let hasMore = $derived(!!nextCursor); 8 let loading = $state(false); 9 const base = import.meta.env.VITE_API_URL; 10 - const endpoint = "/xrpc/org.xcvr.lrc.getMessages"; 11 let query = $derived(`?channelURI=${data.uri}&cursor=${nextCursor}`); 12 $effect(() => console.log(`${base}${endpoint}${query}`)); 13 - $effect(() => console.log(messages)); 14 let scrollContainer: HTMLElement; 15 let shouldScrollToBottom = $state(true); 16 17 $effect(() => { 18 - if (shouldScrollToBottom && scrollContainer && messages.length > 0) { 19 scrollContainer.scrollTop = scrollContainer.scrollHeight; 20 shouldScrollToBottom = false; 21 } ··· 27 const oldScrollTop = scrollContainer.scrollTop; 28 try { 29 const base = import.meta.env.VITE_API_URL; 30 - const endpoint = "/xrpc/org.xcvr.lrc.getMessages"; 31 const query = `?channelURI=${data.uri}&cursor=${nextCursor}`; 32 const res = await fetch(`${base}${endpoint}${query}`); 33 const newData = await res.json(); 34 - messages = [...messages, ...newData.messages]; 35 nextCursor = newData.cursor; 36 requestAnimationFrame(() => { 37 const newScrollHeight = scrollContainer.scrollHeight; ··· 54 <span> loading... </span> 55 {/if} 56 {/if} 57 - {#if messages && messages.length !== 0} 58 - <History {messages} /> 59 {:else} 60 <h1>NO HISTORY</h1> 61 {/if}
··· 1 <script lang="ts"> 2 import type { PageProps } from "./$types"; 3 import History from "$lib/components/History.svelte"; 4 + import type { SignedItemView } from "$lib/types"; 5 + const nicify = (arr: Array<any>): Array<SignedItemView> => { 6 + return arr.map((element) => { 7 + if (element["$type"] === "org.xcvr.lrc.defs#signedMessageView") { 8 + return { ...element, type: "message" }; 9 + } else if (element["$type"] === "org.xcvr.lrc.defs#signedMediaView") 10 + return { ...element, type: "image" }; 11 + }); 12 + }; 13 let { data }: PageProps = $props(); 14 + let items = $state(nicify(data.items)); 15 let nextCursor = $state(data.cursor); 16 let hasMore = $derived(!!nextCursor); 17 let loading = $state(false); 18 const base = import.meta.env.VITE_API_URL; 19 + const endpoint = "/xrpc/org.xcvr.lrc.getHistory"; 20 let query = $derived(`?channelURI=${data.uri}&cursor=${nextCursor}`); 21 $effect(() => console.log(`${base}${endpoint}${query}`)); 22 + $effect(() => console.log(items)); 23 let scrollContainer: HTMLElement; 24 let shouldScrollToBottom = $state(true); 25 26 $effect(() => { 27 + if (shouldScrollToBottom && scrollContainer && items.length > 0) { 28 scrollContainer.scrollTop = scrollContainer.scrollHeight; 29 shouldScrollToBottom = false; 30 } ··· 36 const oldScrollTop = scrollContainer.scrollTop; 37 try { 38 const base = import.meta.env.VITE_API_URL; 39 + const endpoint = "/xrpc/org.xcvr.lrc.getHistory"; 40 const query = `?channelURI=${data.uri}&cursor=${nextCursor}`; 41 const res = await fetch(`${base}${endpoint}${query}`); 42 const newData = await res.json(); 43 + items = [...items, ...nicify(newData.messages)]; 44 nextCursor = newData.cursor; 45 requestAnimationFrame(() => { 46 const newScrollHeight = scrollContainer.scrollHeight; ··· 63 <span> loading... </span> 64 {/if} 65 {/if} 66 + {#if items && items.length !== 0} 67 + <History {items} /> 68 {:else} 69 <h1>NO HISTORY</h1> 70 {/if}
+4 -4
src/routes/c/[handle]/[rkey]/history/+page.ts
··· 17 if (!uri) throw error(500, 'Invalid channel response') 18 19 // Get messages 20 - const messagesRes = await fetch(`${base}/xrpc/org.xcvr.lrc.getMessages?channelURI=${uri}`) 21 - if (!messagesRes.ok) throw error(messagesRes.status, 'Failed to load messages') 22 23 - const { messages = [], cursor } = await messagesRes.json() 24 25 - return { messages, cursor, uri } 26 27 } catch (err) { 28 throw error(500, 'Unexpected error')
··· 17 if (!uri) throw error(500, 'Invalid channel response') 18 19 // Get messages 20 + const historyRes = await fetch(`${base}/xrpc/org.xcvr.lrc.getHistory?channelURI=${uri}`) 21 + if (!historyRes.ok) throw error(historyRes.status, 'Failed to load messages') 22 23 + const { items = [], cursor } = await historyRes.json() 24 25 + return { items, cursor, uri } 26 27 } catch (err) { 28 throw error(500, 'Unexpected error')