groupchat 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +161 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4373,7 +4373,7 @@ var require_react_development = __commonJS({
4373
4373
  var dispatcher = resolveDispatcher();
4374
4374
  return dispatcher.useReducer(reducer, initialArg, init);
4375
4375
  }
4376
- function useRef6(initialValue) {
4376
+ function useRef7(initialValue) {
4377
4377
  var dispatcher = resolveDispatcher();
4378
4378
  return dispatcher.useRef(initialValue);
4379
4379
  }
@@ -5167,7 +5167,7 @@ var require_react_development = __commonJS({
5167
5167
  exports.useLayoutEffect = useLayoutEffect3;
5168
5168
  exports.useMemo = useMemo10;
5169
5169
  exports.useReducer = useReducer;
5170
- exports.useRef = useRef6;
5170
+ exports.useRef = useRef7;
5171
5171
  exports.useState = useState17;
5172
5172
  exports.useSyncExternalStore = useSyncExternalStore;
5173
5173
  exports.useTransition = useTransition;
@@ -35294,7 +35294,7 @@ function Menu({
35294
35294
  isSelected: selectedIndex === newDmIndex
35295
35295
  }
35296
35296
  ),
35297
- isLoadingDms ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Box_default, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { color: "cyan", children: "Loading conversations..." }) }) : dmConversations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Box_default, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { color: "gray", children: "No Direct Messages Yet.." }) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
35297
+ isLoadingDms && dmConversations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Box_default, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { color: "cyan", children: "Loading conversations..." }) }) : dmConversations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Box_default, { marginLeft: 2, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Text, { color: "gray", children: "No Direct Messages Yet.." }) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
35298
35298
  dmConversations.slice(0, 5).map((conversation, idx) => {
35299
35299
  const absoluteIndex = dmStartIndex + idx;
35300
35300
  const isSelected = selectedIndex === absoluteIndex;
@@ -38668,7 +38668,10 @@ function DmConversation({
38668
38668
  const originalOnDmMessage = channelManager["callbacks"].onDmMessage;
38669
38669
  const originalOnDmTypingStart = channelManager["callbacks"].onDmTypingStart;
38670
38670
  const originalOnDmTypingStop = channelManager["callbacks"].onDmTypingStop;
38671
- channelManager["callbacks"].onDmMessage = handleDmMessage;
38671
+ channelManager["callbacks"].onDmMessage = (msg) => {
38672
+ handleDmMessage(msg);
38673
+ originalOnDmMessage?.(msg);
38674
+ };
38672
38675
  channelManager["callbacks"].onDmTypingStart = handleTypingStart2;
38673
38676
  channelManager["callbacks"].onDmTypingStop = handleTypingStop2;
38674
38677
  return () => {
@@ -38679,11 +38682,9 @@ function DmConversation({
38679
38682
  }, [channelManager, dm?.slug, username, isScrollDetached]);
38680
38683
  const handleSendMessage = (0, import_react38.useCallback)(async (content) => {
38681
38684
  if (!channelManager || !dm) return;
38682
- try {
38683
- await channelManager.sendDmMessage(dm.slug, content);
38684
- } catch (err) {
38685
+ channelManager.sendDmMessage(dm.slug, content).catch(() => {
38685
38686
  setError("Failed to send message");
38686
- }
38687
+ });
38687
38688
  }, [channelManager, dm]);
38688
38689
  const handleTypingStart = (0, import_react38.useCallback)(() => {
38689
38690
  if (!channelManager || !dm) return;
@@ -38801,6 +38802,57 @@ var init_DmConversation = __esm({
38801
38802
  }
38802
38803
  });
38803
38804
 
38805
+ // src/lib/presence-utils.ts
38806
+ function upsertMetas(existing, incoming) {
38807
+ if (incoming.length === 0) return existing;
38808
+ const merged = existing.slice();
38809
+ const indexByRef = /* @__PURE__ */ new Map();
38810
+ existing.forEach((meta, index) => {
38811
+ indexByRef.set(meta.phx_ref, index);
38812
+ });
38813
+ incoming.forEach((meta) => {
38814
+ const index = indexByRef.get(meta.phx_ref);
38815
+ if (index === void 0) {
38816
+ indexByRef.set(meta.phx_ref, merged.length);
38817
+ merged.push(meta);
38818
+ } else {
38819
+ merged[index] = meta;
38820
+ }
38821
+ });
38822
+ return merged;
38823
+ }
38824
+ function removeMetas(existing, leaving) {
38825
+ if (leaving.length === 0) return existing;
38826
+ const refsToRemove = new Set(leaving.map((meta) => meta.phx_ref));
38827
+ return existing.filter((meta) => !refsToRemove.has(meta.phx_ref));
38828
+ }
38829
+ function applyPresenceDiff(prev, diff2) {
38830
+ const next = { ...prev };
38831
+ Object.entries(diff2.leaves).forEach(([username, data]) => {
38832
+ const existing = next[username]?.metas;
38833
+ if (!existing || existing.length === 0) return;
38834
+ const remaining = removeMetas(existing, data.metas);
38835
+ if (remaining.length === 0) {
38836
+ delete next[username];
38837
+ } else {
38838
+ next[username] = { metas: remaining };
38839
+ }
38840
+ });
38841
+ Object.entries(diff2.joins).forEach(([username, data]) => {
38842
+ const existing = next[username]?.metas ?? [];
38843
+ const merged = upsertMetas(existing, data.metas);
38844
+ if (merged.length > 0) {
38845
+ next[username] = { metas: merged };
38846
+ }
38847
+ });
38848
+ return next;
38849
+ }
38850
+ var init_presence_utils = __esm({
38851
+ "src/lib/presence-utils.ts"() {
38852
+ "use strict";
38853
+ }
38854
+ });
38855
+
38804
38856
  // src/lib/channel-manager.ts
38805
38857
  function extractTimestampFromUUIDv7(uuid) {
38806
38858
  const hex = uuid.replace(/-/g, "").slice(0, 12);
@@ -38812,6 +38864,7 @@ var init_channel_manager = __esm({
38812
38864
  "src/lib/channel-manager.ts"() {
38813
38865
  "use strict";
38814
38866
  init_phoenix();
38867
+ init_presence_utils();
38815
38868
  if (typeof globalThis.WebSocket === "undefined") {
38816
38869
  throw new Error(
38817
38870
  "WebSocket is not available. Load the ws polyfill before ChannelManager."
@@ -38925,10 +38978,7 @@ var init_channel_manager = __esm({
38925
38978
  });
38926
38979
  this.statusChannel.on("presence_diff", (payload) => {
38927
38980
  const diff2 = payload;
38928
- const next = { ...this.globalPresence };
38929
- Object.keys(diff2.leaves).forEach((u) => delete next[u]);
38930
- Object.entries(diff2.joins).forEach(([u, d]) => next[u] = d);
38931
- this.globalPresence = next;
38981
+ this.globalPresence = applyPresenceDiff(this.globalPresence, diff2);
38932
38982
  this.callbacks.onGlobalPresenceDiff?.(diff2);
38933
38983
  });
38934
38984
  this.statusChannel.join().receive("ok", () => resolve()).receive("error", (e) => reject(e)).receive("timeout", () => reject(new Error("timeout")));
@@ -39036,14 +39086,7 @@ var init_channel_manager = __esm({
39036
39086
  const diff2 = payload;
39037
39087
  const channelState = this.channelStates.get(channelSlug);
39038
39088
  if (channelState) {
39039
- const next = { ...channelState.presence };
39040
- Object.keys(diff2.leaves).forEach((username) => {
39041
- delete next[username];
39042
- });
39043
- Object.entries(diff2.joins).forEach(([username, data]) => {
39044
- next[username] = data;
39045
- });
39046
- channelState.presence = next;
39089
+ channelState.presence = applyPresenceDiff(channelState.presence, diff2);
39047
39090
  }
39048
39091
  if (channelSlug === this.currentActiveChannel) {
39049
39092
  this.callbacks.onPresenceDiff?.(channelSlug, diff2);
@@ -39607,16 +39650,7 @@ function useMultiChannelChat(token, currentChannel, onChannelListChanged, increm
39607
39650
  });
39608
39651
  },
39609
39652
  onPresenceDiff: (channelSlug, diff2) => {
39610
- setPresenceState((prev) => {
39611
- const next = { ...prev };
39612
- Object.keys(diff2.leaves).forEach((username2) => {
39613
- delete next[username2];
39614
- });
39615
- Object.entries(diff2.joins).forEach(([username2, data]) => {
39616
- next[username2] = data;
39617
- });
39618
- return next;
39619
- });
39653
+ setPresenceState((prev) => applyPresenceDiff(prev, diff2));
39620
39654
  },
39621
39655
  onUserTyping: (channelSlug, username2, typing) => {
39622
39656
  setTypingUsers((prev) => {
@@ -39694,12 +39728,7 @@ function useMultiChannelChat(token, currentChannel, onChannelListChanged, increm
39694
39728
  setGlobalPresence(state);
39695
39729
  },
39696
39730
  onGlobalPresenceDiff: (diff2) => {
39697
- setGlobalPresence((prev) => {
39698
- const next = { ...prev };
39699
- Object.keys(diff2.leaves).forEach((u) => delete next[u]);
39700
- Object.entries(diff2.joins).forEach(([u, d]) => next[u] = d);
39701
- return next;
39702
- });
39731
+ setGlobalPresence((prev) => applyPresenceDiff(prev, diff2));
39703
39732
  }
39704
39733
  }
39705
39734
  );
@@ -39836,6 +39865,7 @@ var init_use_multi_channel_chat = __esm({
39836
39865
  init_chat_client();
39837
39866
  init_config();
39838
39867
  init_notification_manager();
39868
+ init_presence_utils();
39839
39869
  }
39840
39870
  });
39841
39871
 
@@ -40025,11 +40055,37 @@ var init_use_channels = __esm({
40025
40055
  }
40026
40056
  });
40027
40057
 
40058
+ // src/lib/dm-utils.ts
40059
+ function truncatePreview(content) {
40060
+ if (content.length > 100) {
40061
+ return content.slice(0, 97) + "...";
40062
+ }
40063
+ return content;
40064
+ }
40065
+ function sortConversationsByActivity(conversations) {
40066
+ return [...conversations].sort((a, b) => {
40067
+ return new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime();
40068
+ });
40069
+ }
40070
+ var init_dm_utils = __esm({
40071
+ "src/lib/dm-utils.ts"() {
40072
+ "use strict";
40073
+ }
40074
+ });
40075
+
40028
40076
  // src/hooks/use-dms.ts
40029
- function useDms(token) {
40077
+ function useDms(token, channelManager, currentUsername, activeDmSlug) {
40030
40078
  const [conversations, setConversations] = (0, import_react43.useState)([]);
40031
40079
  const [loading, setLoading] = (0, import_react43.useState)(false);
40032
40080
  const [error, setError] = (0, import_react43.useState)(null);
40081
+ const activeDmSlugRef = (0, import_react43.useRef)(activeDmSlug);
40082
+ const currentUsernameRef = (0, import_react43.useRef)(currentUsername);
40083
+ (0, import_react43.useEffect)(() => {
40084
+ activeDmSlugRef.current = activeDmSlug;
40085
+ }, [activeDmSlug]);
40086
+ (0, import_react43.useEffect)(() => {
40087
+ currentUsernameRef.current = currentUsername;
40088
+ }, [currentUsername]);
40033
40089
  const fetchData = (0, import_react43.useCallback)(async () => {
40034
40090
  if (!token) return;
40035
40091
  setLoading(true);
@@ -40037,10 +40093,7 @@ function useDms(token) {
40037
40093
  try {
40038
40094
  const config = getConfig();
40039
40095
  const data = await fetchDmConversations(config.wsUrl, token);
40040
- const sortedConversations = [...data.conversations].sort((a, b) => {
40041
- return new Date(b.last_activity_at).getTime() - new Date(a.last_activity_at).getTime();
40042
- });
40043
- setConversations(sortedConversations);
40096
+ setConversations(sortConversationsByActivity(data.conversations));
40044
40097
  } catch (err) {
40045
40098
  setError(err instanceof Error ? err.message : "Failed to fetch DM conversations");
40046
40099
  } finally {
@@ -40050,6 +40103,56 @@ function useDms(token) {
40050
40103
  const totalDmUnreadCount = (0, import_react43.useMemo)(() => {
40051
40104
  return conversations.reduce((sum, conv) => sum + conv.unread_count, 0);
40052
40105
  }, [conversations]);
40106
+ const clearUnreadCount = (0, import_react43.useCallback)((dmSlug) => {
40107
+ setConversations(
40108
+ (prev) => prev.map(
40109
+ (conv) => conv.slug === dmSlug ? { ...conv, unread_count: 0 } : conv
40110
+ )
40111
+ );
40112
+ }, []);
40113
+ const handleDmMessage = (0, import_react43.useCallback)((msg) => {
40114
+ const now = (/* @__PURE__ */ new Date()).toISOString();
40115
+ const preview = truncatePreview(msg.content);
40116
+ const isActiveConversation = activeDmSlugRef.current === msg.dm_slug;
40117
+ const isOwnMessage = msg.username === currentUsernameRef.current;
40118
+ setConversations((prev) => {
40119
+ const existingIndex = prev.findIndex((conv) => conv.slug === msg.dm_slug);
40120
+ if (existingIndex >= 0) {
40121
+ const updated = prev.filter((_, idx) => idx !== existingIndex);
40122
+ const conversation = {
40123
+ ...prev[existingIndex],
40124
+ last_activity_at: now,
40125
+ last_message_preview: preview,
40126
+ unread_count: isActiveConversation || isOwnMessage ? prev[existingIndex].unread_count : prev[existingIndex].unread_count + 1
40127
+ };
40128
+ return [conversation, ...updated];
40129
+ } else {
40130
+ const newConversation = {
40131
+ channel_id: msg.dm_slug,
40132
+ slug: msg.dm_slug,
40133
+ other_user_id: msg.sender_id,
40134
+ other_username: msg.username,
40135
+ last_activity_at: now,
40136
+ last_message_preview: preview,
40137
+ unread_count: isOwnMessage ? 0 : 1
40138
+ };
40139
+ return [newConversation, ...prev];
40140
+ }
40141
+ });
40142
+ if (!isOwnMessage && !isActiveConversation) {
40143
+ getNotificationManager().notify("bell");
40144
+ }
40145
+ }, []);
40146
+ (0, import_react43.useEffect)(() => {
40147
+ if (!channelManager) {
40148
+ return;
40149
+ }
40150
+ const originalCallback = channelManager["callbacks"].onDmMessage;
40151
+ channelManager["callbacks"].onDmMessage = handleDmMessage;
40152
+ return () => {
40153
+ channelManager["callbacks"].onDmMessage = originalCallback;
40154
+ };
40155
+ }, [channelManager, handleDmMessage]);
40053
40156
  (0, import_react43.useEffect)(() => {
40054
40157
  if (token) {
40055
40158
  fetchData();
@@ -40060,7 +40163,8 @@ function useDms(token) {
40060
40163
  loading,
40061
40164
  error,
40062
40165
  refetch: fetchData,
40063
- totalDmUnreadCount
40166
+ totalDmUnreadCount,
40167
+ clearUnreadCount
40064
40168
  };
40065
40169
  }
40066
40170
  var import_react43;
@@ -40070,6 +40174,8 @@ var init_use_dms = __esm({
40070
40174
  import_react43 = __toESM(require_react(), 1);
40071
40175
  init_chat_client();
40072
40176
  init_config();
40177
+ init_notification_manager();
40178
+ init_dm_utils();
40073
40179
  }
40074
40180
  });
40075
40181
 
@@ -40101,6 +40207,7 @@ function AppContent() {
40101
40207
  const prevAuthStateRef = (0, import_react44.useRef)(null);
40102
40208
  const [currentDm, setCurrentDm] = (0, import_react44.useState)(null);
40103
40209
  const [shouldStartDmSearch, setShouldStartDmSearch] = (0, import_react44.useState)(false);
40210
+ const activeDmSlug = route === "dm-chat" && currentDm ? currentDm.slug : null;
40104
40211
  (0, import_react44.useEffect)(() => {
40105
40212
  if (stdout) {
40106
40213
  getNotificationManager().setStdout(stdout);
@@ -40140,13 +40247,6 @@ function AppContent() {
40140
40247
  checkAuth();
40141
40248
  }, []);
40142
40249
  const { publicChannels, privateChannels, unreadCounts, loading: isLoadingChannels, refetchUnreadCounts, refetch: refetchChannels, incrementUnreadCount, clearUnreadCount, totalUnreadCount } = useChannels(token);
40143
- const {
40144
- conversations: dmConversations,
40145
- loading: isLoadingDms,
40146
- totalDmUnreadCount,
40147
- refetch: refetchDms
40148
- } = useDms(token);
40149
- const combinedTotalUnreadCount = totalUnreadCount + totalDmUnreadCount;
40150
40250
  const {
40151
40251
  messages,
40152
40252
  connectionStatus,
@@ -40163,6 +40263,14 @@ function AppContent() {
40163
40263
  disconnect,
40164
40264
  channelManager
40165
40265
  } = useMultiChannelChat(token, currentChannel, refetchChannels, incrementUnreadCount);
40266
+ const {
40267
+ conversations: dmConversations,
40268
+ loading: isLoadingDms,
40269
+ totalDmUnreadCount,
40270
+ refetch: refetchDms,
40271
+ clearUnreadCount: clearDmUnreadCount
40272
+ } = useDms(token, channelManager, username, activeDmSlug);
40273
+ const combinedTotalUnreadCount = totalUnreadCount + totalDmUnreadCount;
40166
40274
  const { users } = usePresence(presenceState, subscribers, currentChannel, globalPresence);
40167
40275
  useAgentDetection(channelManager, connectionStatus === "connected");
40168
40276
  const sendCommand = (0, import_react44.useCallback)(
@@ -40273,8 +40381,9 @@ function AppContent() {
40273
40381
  }, [token, refetchChannels]);
40274
40382
  const handleDmSelect = (0, import_react44.useCallback)((dm) => {
40275
40383
  setCurrentDm(dm);
40384
+ clearDmUnreadCount(dm.slug);
40276
40385
  navigate("dm-chat");
40277
- }, [navigate]);
40386
+ }, [navigate, clearDmUnreadCount]);
40278
40387
  const handleNewDm = (0, import_react44.useCallback)(() => {
40279
40388
  setShouldStartDmSearch(true);
40280
40389
  navigate("dm-inbox");
@@ -40407,6 +40516,7 @@ function AppContent() {
40407
40516
  if (route === "dm-inbox") {
40408
40517
  const handleSelectDm = (dm) => {
40409
40518
  setCurrentDm(dm);
40519
+ clearDmUnreadCount(dm.slug);
40410
40520
  navigate("dm-chat");
40411
40521
  };
40412
40522
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -40705,7 +40815,7 @@ function UpdatePrompt({ updateInfo, onComplete }) {
40705
40815
  // package.json
40706
40816
  var package_default = {
40707
40817
  name: "groupchat",
40708
- version: "0.0.14",
40818
+ version: "0.0.15",
40709
40819
  description: "CLI chat client for Groupchat",
40710
40820
  type: "module",
40711
40821
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groupchat",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "CLI chat client for Groupchat",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",