cheogram-www
created pr with
cheogram-www/multi-dm.1
changed pr name to
New UI for starting chats with multiDM support
added cheogram-www/multi-dm.2
1: 8f55142 < -: ------- Add UI to create private channel
2: ea160a8 ! 1: e758cfd New UI for starting chats
3: faa9d23 = 2: eb5c50d Special case for group texts
4: 0d9f850 = 3: 2217816 Only enable non-group-text multiDM experimentally
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:multi-dm | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:multi-dm [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:multi-dmadd comment to patch request:
ssh pr.pico.sh comment cheogram-www:multi-dm "lgtm!"
Patchset
cheogram-www/multi-dm.2
Special case for group texts
Stephen Paul Weber
2026-11-10T19:02:07ZSemantic diff summary
0 added,
6 modified,
0 signature changed,
0 removed
across 2 analyzed files
+56
-0
src/components/NewChatDialog.test.tsx
#
| ... | ... | @@ -152,6 +152,62 @@ describe("NewChatDialog", () => { | |
| 152 | 152 | expect(startChat).toHaveBeenCalledWith(alice); | |
| 153 | 153 | }); | |
| 154 | 154 | ||
| 155 | + | it("replaces Cheogram selections when mixing with other counterparts", async () => { | |
| 156 | + | const sms = availableChat("15551234567@cheogram.com", "SMS"); | |
| 157 | + | const alice = availableChat("alice@example.com", "Alice"); | |
| 158 | + | const { startChat, onStarted, chat } = setup([sms, alice]); | |
| 159 | + | ||
| 160 | + | fireEvent.click(await search("example.com", "SMS")); | |
| 161 | + | fireEvent.click(screen.getByText("Alice")); | |
| 162 | + | ||
| 163 | + | expect(screen.queryByRole("alert")).toBeNull(); | |
| 164 | + | ||
| 165 | + | fireEvent.click(screen.getByRole("button", { name: "Start Chat" })); | |
| 166 | + | ||
| 167 | + | await waitFor(() => expect(onStarted).toHaveBeenCalledWith(chat)); | |
| 168 | + | expect(startChat).toHaveBeenCalledWith(alice); | |
| 169 | + | }); | |
| 170 | + | ||
| 171 | + | it("starts a group text with a sorted Cheogram target", async () => { | |
| 172 | + | const first = availableChat("789@cheogram.com", "Seven"); | |
| 173 | + | const second = availableChat("123@cheogram.com", "One"); | |
| 174 | + | const { startMultiChat, onStarted, multiChat } = setup([first, second]); | |
| 175 | + | ||
| 176 | + | fireEvent.click(await search("cheogram.com", "Seven")); | |
| 177 | + | fireEvent.click(screen.getByText("One")); | |
| 178 | + | ||
| 179 | + | fireEvent.click(screen.getByRole("button", { name: "Start Group Text" })); | |
| 180 | + | ||
| 181 | + | await waitFor(() => expect(onStarted).toHaveBeenCalledWith(multiChat)); | |
| 182 | + | expect(startMultiChat).toHaveBeenCalledWith( | |
| 183 | + | [first, second], | |
| 184 | + | "123,789@cheogram.com", | |
| 185 | + | ); | |
| 186 | + | }); | |
| 187 | + | ||
| 188 | + | it("deduplicates comma-separated Cheogram targets", async () => { | |
| 189 | + | const first = availableChat("789@cheogram.com", "Seven"); | |
| 190 | + | const group = availableChat("123,456@cheogram.com", "Group"); | |
| 191 | + | const duplicate = availableChat("456@cheogram.com", "Four"); | |
| 192 | + | const { startMultiChat, onStarted, multiChat } = setup([ | |
| 193 | + | first, | |
| 194 | + | group, | |
| 195 | + | duplicate, | |
| 196 | + | ]); | |
| 197 | + | ||
| 198 | + | fireEvent.click(await search("cheogram.com", "Seven")); | |
| 199 | + | fireEvent.click(screen.getByText("Group")); | |
| 200 | + | fireEvent.click(screen.getByText("Four")); | |
| 201 | + | ||
| 202 | + | fireEvent.click(screen.getByRole("button", { name: "Start Group Text" })); | |
| 203 | + | ||
| 204 | + | await waitFor(() => expect(onStarted).toHaveBeenCalledWith(multiChat)); | |
| 205 | + | expect(startMultiChat).toHaveBeenCalledWith( | |
| 206 | + | [first, group, duplicate], | |
| 207 | + | "123,456,789@cheogram.com", | |
| 208 | + | ); | |
| 209 | + | }); | |
| 210 | + | ||
| 155 | 211 | it("does not mix channels with other selections", async () => { | |
| 156 | 212 | const alice = availableChat("alice@example.com", "Alice"); | |
| 157 | 213 | const channel = availableChat("room@example.com", "Room", true); |
+37
-4
src/components/NewChatDialog.tsx
#
| ... | ... | @@ -20,6 +20,11 @@ import { useDebounce } from "../hooks/use-debounce"; | |
| 20 | 20 | ||
| 21 | 21 | import { findAvailableChats } from "../util"; | |
| 22 | 22 | ||
| 23 | + | const CHEOGRAM_DOMAIN = "@cheogram.com"; | |
| 24 | + | ||
| 25 | + | const isCheogramChat = (availableChat: borogove.AvailableChat) => | |
| 26 | + | availableChat.chatId.endsWith(CHEOGRAM_DOMAIN); | |
| 27 | + | ||
| 23 | 28 | export const NewChatDialog = ({ | |
| 24 | 29 | open, | |
| 25 | 30 | client, |
| ... | ... | @@ -111,9 +116,15 @@ export const NewChatDialog = ({ | |
| 111 | 116 | return existing.filter((c) => c.chatId !== availableChat.chatId); | |
| 112 | 117 | } | |
| 113 | 118 | ||
| 114 | - | // A multiDM cannot include a channel, so a channel selection | |
| 119 | + | // A multiDM cannot include a channel, and Cheogram contacts can only be | |
| 120 | + | // grouped with other Cheogram contacts, so an incompatible selection | |
| 115 | 121 | // replaces any other selections. | |
| 116 | - | if (availableChat.isChannel() || existing.some((c) => c.isChannel())) { | |
| 122 | + | if ( | |
| 123 | + | availableChat.isChannel() || | |
| 124 | + | existing.some((c) => c.isChannel()) || | |
| 125 | + | (existing.length > 0 && | |
| 126 | + | isCheogramChat(availableChat) !== existing.every(isCheogramChat)) | |
| 127 | + | ) { | |
| 117 | 128 | return [availableChat]; | |
| 118 | 129 | } | |
| 119 | 130 |
| ... | ... | @@ -121,6 +132,22 @@ export const NewChatDialog = ({ | |
| 121 | 132 | }); | |
| 122 | 133 | }; | |
| 123 | 134 | ||
| 135 | + | const allCheogram = selected.length > 1 && selected.every(isCheogramChat); | |
| 136 | + | const groupTextTarget = allCheogram | |
| 137 | + | ? [ | |
| 138 | + | ...new Set( | |
| 139 | + | selected.flatMap((c) => | |
| 140 | + | c.chatId | |
| 141 | + | .slice(0, -CHEOGRAM_DOMAIN.length) | |
| 142 | + | .split(",") | |
| 143 | + | .filter((part) => part !== ""), | |
| 144 | + | ), | |
| 145 | + | ), | |
| 146 | + | ] | |
| 147 | + | .sort() | |
| 148 | + | .join(",") + CHEOGRAM_DOMAIN | |
| 149 | + | : null; | |
| 150 | + | ||
| 124 | 151 | const startChat = (event: React.FormEvent<HTMLFormElement>) => { | |
| 125 | 152 | event.preventDefault(); | |
| 126 | 153 | if (starting || selected.length === 0) return; |
| ... | ... | @@ -132,7 +159,9 @@ export const NewChatDialog = ({ | |
| 132 | 159 | const chat = | |
| 133 | 160 | selected.length === 1 | |
| 134 | 161 | ? client.startChat(selected[0]) | |
| 135 | - | : client.startMultiChat(selected); | |
| 162 | + | : groupTextTarget | |
| 163 | + | ? client.startMultiChat(selected, groupTextTarget) | |
| 164 | + | : client.startMultiChat(selected); | |
| 136 | 165 | reset(); | |
| 137 | 166 | onStarted(chat); | |
| 138 | 167 | } catch (e) { |
| ... | ... | @@ -234,7 +263,11 @@ export const NewChatDialog = ({ | |
| 234 | 263 | variant="contained" | |
| 235 | 264 | disabled={starting || selected.length === 0} | |
| 236 | 265 | > | |
| 237 | - | {starting ? "Starting..." : "Start Chat"} | |
| 266 | + | {starting | |
| 267 | + | ? "Starting..." | |
| 268 | + | : allCheogram | |
| 269 | + | ? "Start Group Text" | |
| 270 | + | : "Start Chat"} | |
| 238 | 271 | </Button> | |
| 239 | 272 | </DialogActions> | |
| 240 | 273 | </form> |