BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 220 lines 6.6 kB view raw
1import { asModerationLabels } from "$/lib/moderation"; 2import type { 3 ConvoView, 4 DeletedMessageView, 5 GetConvoForMembersResponse, 6 GetMessagesResponse, 7 ListConvosResponse, 8 MessageView, 9 ProfileViewBasic, 10} from "$/lib/types"; 11import { invoke } from "@tauri-apps/api/core"; 12import { asArray, asRecord } from "../type-guards"; 13 14function optionalNumber(value: unknown) { 15 return typeof value === "number" ? value : null; 16} 17 18function optionalString(value: unknown) { 19 return typeof value === "string" ? value : null; 20} 21 22function parseProfileBasic(value: unknown): ProfileViewBasic | null { 23 const record = asRecord(value); 24 if (!record || typeof record.did !== "string" || typeof record.handle !== "string") { 25 return null; 26 } 27 28 const viewer = asRecord(record.viewer); 29 30 return { 31 avatar: optionalString(record.avatar), 32 did: record.did, 33 displayName: optionalString(record.displayName), 34 handle: record.handle, 35 labels: asModerationLabels(record), 36 viewer: viewer ? { following: optionalString(viewer.following) } : null, 37 }; 38} 39 40function parseMessageSender(value: unknown) { 41 const record = asRecord(value); 42 if (!record || typeof record.did !== "string") { 43 return null; 44 } 45 46 return { did: record.did }; 47} 48 49function parseDeletedMessageView(value: unknown): DeletedMessageView | null { 50 const record = asRecord(value); 51 const sender = parseMessageSender(record?.sender); 52 if ( 53 !record || !sender || typeof record.id !== "string" || typeof record.rev !== "string" 54 || typeof record.sentAt !== "string" 55 ) { 56 return null; 57 } 58 59 return { 60 $type: optionalString(record.$type) as DeletedMessageView["$type"], 61 id: record.id, 62 rev: record.rev, 63 sender, 64 sentAt: record.sentAt, 65 }; 66} 67 68function parseMessageView(value: unknown): MessageView | null { 69 const record = asRecord(value); 70 const sender = parseMessageSender(record?.sender); 71 if ( 72 !record 73 || !sender 74 || typeof record.id !== "string" 75 || typeof record.rev !== "string" 76 || typeof record.sentAt !== "string" 77 || typeof record.text !== "string" 78 ) { 79 return null; 80 } 81 82 return { 83 $type: optionalString(record.$type) as MessageView["$type"], 84 id: record.id, 85 rev: record.rev, 86 sender, 87 sentAt: record.sentAt, 88 text: record.text, 89 }; 90} 91 92function parseConvoMessage(value: unknown) { 93 const record = asRecord(value); 94 if (!record) { 95 return null; 96 } 97 98 if (record.$type === "chat.bsky.convo.defs#deletedMessageView") { 99 return parseDeletedMessageView(record); 100 } 101 102 return parseMessageView(record); 103} 104 105function parseConvoView(value: unknown): ConvoView | null { 106 const record = asRecord(value); 107 const rawMembers = asArray(record?.members); 108 if ( 109 !record 110 || !rawMembers 111 || typeof record.id !== "string" 112 || typeof record.rev !== "string" 113 || typeof record.muted !== "boolean" 114 ) { 115 return null; 116 } 117 118 const members = rawMembers.map((member) => parseProfileBasic(member)); 119 if (members.some((member) => !member)) { 120 return null; 121 } 122 123 const lastMessage = record.lastMessage === null || record.lastMessage === undefined 124 ? null 125 : parseConvoMessage(record.lastMessage); 126 if (record.lastMessage !== null && record.lastMessage !== undefined && !lastMessage) { 127 return null; 128 } 129 130 return { 131 id: record.id, 132 lastMessage, 133 members: members as ProfileViewBasic[], 134 muted: record.muted, 135 rev: record.rev, 136 status: optionalString(record.status), 137 unreadCount: optionalNumber(record.unreadCount) ?? 0, 138 }; 139} 140 141export function parseListConvosResponse(value: unknown): ListConvosResponse { 142 const record = asRecord(value); 143 const rawConvos = asArray(record?.convos); 144 if (!record || !rawConvos) { 145 throw new Error("conversations response payload is invalid"); 146 } 147 148 const convos = rawConvos.map((convo) => parseConvoView(convo)); 149 if (convos.some((convo) => !convo)) { 150 throw new Error("conversations response contains an invalid conversation"); 151 } 152 153 if (record.cursor !== undefined && record.cursor !== null && typeof record.cursor !== "string") { 154 throw new Error("conversations response cursor is invalid"); 155 } 156 157 return { convos: convos as ConvoView[], cursor: optionalString(record.cursor) }; 158} 159 160export function parseGetConvoForMembersResponse(value: unknown): GetConvoForMembersResponse { 161 const record = asRecord(value); 162 const convo = parseConvoView(record?.convo); 163 if (!record || !convo) { 164 throw new Error("conversation payload is invalid"); 165 } 166 167 return { convo }; 168} 169 170export function parseGetMessagesResponse(value: unknown): GetMessagesResponse { 171 const record = asRecord(value); 172 const rawMessages = asArray(record?.messages); 173 if (!record || !rawMessages) { 174 throw new Error("messages response payload is invalid"); 175 } 176 177 const messages = rawMessages.map((message) => parseConvoMessage(message)); 178 if (messages.some((message) => !message)) { 179 throw new Error("messages response contains an invalid message"); 180 } 181 182 if (record.cursor !== undefined && record.cursor !== null && typeof record.cursor !== "string") { 183 throw new Error("messages response cursor is invalid"); 184 } 185 186 return { cursor: optionalString(record.cursor), messages: messages as Array<MessageView | DeletedMessageView> }; 187} 188 189export function parseSendMessageResponse(value: unknown): MessageView { 190 const message = parseMessageView(value); 191 if (!message) { 192 throw new Error("sent message payload is invalid"); 193 } 194 195 return message; 196} 197 198async function listConvos(cursor?: string | null, limit?: number): Promise<ListConvosResponse> { 199 return invoke("list_convos", { cursor: cursor ?? null, limit: limit ?? null }).then(parseListConvosResponse); 200} 201 202async function getConvoForMembers(members: string[]): Promise<GetConvoForMembersResponse> { 203 return invoke("get_convo_for_members", { members }).then(parseGetConvoForMembersResponse); 204} 205 206async function getMessages(convoId: string, cursor?: string | null, limit?: number): Promise<GetMessagesResponse> { 207 return invoke("get_messages", { convoId, cursor: cursor ?? null, limit: limit ?? null }).then( 208 parseGetMessagesResponse, 209 ); 210} 211 212async function sendMessage(convoId: string, text: string): Promise<MessageView> { 213 return invoke("send_message", { convoId, text }).then(parseSendMessageResponse); 214} 215 216async function updateRead(convoId: string, messageId?: string | null): Promise<void> { 217 return invoke("update_read", { convoId, messageId: messageId ?? null }); 218} 219 220export const ConvoController = { listConvos, getConvoForMembers, getMessages, sendMessage, updateRead };