Skip to content

Commit c81e4ea

Browse files
committed
Merge branch 'master' into fs2
2 parents a3b7fec + b7ac9ef commit c81e4ea

File tree

15 files changed

+1200
-453
lines changed

15 files changed

+1200
-453
lines changed

src/packages/frontend/app/context.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ export function useAntdStyleProvider() {
9696

9797
const algorithm = compact ? { algorithm: theme.compactAlgorithm } : undefined;
9898

99+
const textColors = { colorTextDescription: COLORS.GRAY_DD };
100+
99101
const antdTheme: ThemeConfig = {
100102
...algorithm,
101103
token: {
104+
...textColors,
102105
...brandedColors,
103106
...borderStyle,
104107
...animationStyle,

src/packages/frontend/chat/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Note: There are a couple of ways to represent a time in Javascript:
1616

1717
The data structures for chat have somehow evolved since that
1818
crazy Sage Days by the Ocean in WA to use all of these at once, which is
19-
confusing and annoying. Be careful!
19+
confusing and annoying. Be careful!
2020

2121
## Overview
2222

src/packages/frontend/chat/actions.ts

Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,18 @@ export class ChatActions extends Actions<ChatState> {
168168
tag,
169169
noNotification,
170170
submitMentionsRef,
171+
extraInput,
172+
name,
171173
}: {
172174
input?: string;
173175
sender_id?: string;
174176
reply_to?: Date;
175177
tag?: string;
176178
noNotification?: boolean;
177179
submitMentionsRef?;
180+
extraInput?: string;
181+
// if name is given, rename thread to have that name
182+
name?: string;
178183
}): string => {
179184
if (this.syncdb == null || this.store == null) {
180185
console.warn("attempt to sendChat before chat actions initialized");
@@ -186,11 +191,15 @@ export class ChatActions extends Actions<ChatState> {
186191
if (submitMentionsRef?.current != null) {
187192
input = submitMentionsRef.current?.({ chat: `${time_stamp.valueOf()}` });
188193
}
194+
if (extraInput) {
195+
input = (input ?? "") + extraInput;
196+
}
189197
input = input?.trim();
190198
if (!input) {
191199
// do not send when there is nothing to send.
192200
return "";
193201
}
202+
const trimmedName = name?.trim();
194203
const message: ChatMessage = {
195204
sender_id,
196205
event: "chat",
@@ -205,7 +214,12 @@ export class ChatActions extends Actions<ChatState> {
205214
reply_to: reply_to?.toISOString(),
206215
editing: {},
207216
};
217+
if (trimmedName && !reply_to) {
218+
(message as any).name = trimmedName;
219+
}
208220
this.syncdb.set(message);
221+
const messagesState = this.store.get("messages");
222+
let selectedThreadKey: string;
209223
if (!reply_to) {
210224
this.deleteDraft(0);
211225
// NOTE: we also clear search, since it's confusing to send a message and not
@@ -214,17 +228,29 @@ export class ChatActions extends Actions<ChatState> {
214228
// Also, only do this clearing when not replying.
215229
// For replies search find full threads not individual messages.
216230
this.clearAllFilters();
231+
selectedThreadKey = `${time_stamp.valueOf()}`;
217232
} else {
218233
// when replying we make sure that the thread is expanded, since otherwise
219234
// our reply won't be visible
220-
const messages = this.store.get("messages");
221235
if (
222-
messages
236+
messagesState
223237
?.getIn([`${reply_to.valueOf()}`, "folding"])
224238
?.includes(sender_id)
225239
) {
226240
this.toggleFoldThread(reply_to);
227241
}
242+
const root =
243+
getThreadRootDate({
244+
date: reply_to.valueOf(),
245+
messages: messagesState,
246+
}) ?? reply_to.valueOf();
247+
selectedThreadKey = `${root}`;
248+
}
249+
if (selectedThreadKey != "0") {
250+
this.setSelectedThread(selectedThreadKey);
251+
}
252+
if (trimmedName && reply_to) {
253+
this.renameThread(selectedThreadKey, trimmedName);
228254
}
229255

230256
const project_id = this.store?.get("project_id");
@@ -481,6 +507,127 @@ export class ChatActions extends Actions<ChatState> {
481507
});
482508
};
483509

510+
// returns number of deleted messages
511+
// threadKey = iso timestamp root of thread.
512+
deleteThread = (threadKey: string): number => {
513+
if (this.syncdb == null || this.store == null) {
514+
return 0;
515+
}
516+
const messages = this.store.get("messages");
517+
if (messages == null) {
518+
return 0;
519+
}
520+
const rootTarget = parseInt(`${threadKey}`);
521+
if (!isFinite(rootTarget)) {
522+
return 0;
523+
}
524+
let deleted = 0;
525+
for (const [_, message] of messages) {
526+
if (message == null) continue;
527+
const dateField = message.get("date");
528+
let dateValue: number | undefined;
529+
let dateIso: string | undefined;
530+
if (dateField instanceof Date) {
531+
dateValue = dateField.valueOf();
532+
dateIso = dateField.toISOString();
533+
} else if (typeof dateField === "number") {
534+
dateValue = dateField;
535+
dateIso = new Date(dateField).toISOString();
536+
} else if (typeof dateField === "string") {
537+
const t = Date.parse(dateField);
538+
dateValue = isNaN(t) ? undefined : t;
539+
dateIso = dateField;
540+
}
541+
if (dateValue == null || dateIso == null) {
542+
continue;
543+
}
544+
const rootDate =
545+
getThreadRootDate({ date: dateValue, messages }) || dateValue;
546+
if (rootDate !== rootTarget) {
547+
continue;
548+
}
549+
this.syncdb.delete({ event: "chat", date: dateIso });
550+
deleted++;
551+
}
552+
if (deleted > 0) {
553+
this.syncdb.commit();
554+
}
555+
return deleted;
556+
};
557+
558+
renameThread = (threadKey: string, name: string): boolean => {
559+
if (this.syncdb == null) {
560+
return false;
561+
}
562+
const entry = this.getThreadRootDoc(threadKey);
563+
if (entry == null) {
564+
return false;
565+
}
566+
const trimmed = name.trim();
567+
if (trimmed) {
568+
entry.doc.name = trimmed;
569+
} else {
570+
delete entry.doc.name;
571+
}
572+
this.syncdb.set(entry.doc);
573+
this.syncdb.commit();
574+
return true;
575+
};
576+
577+
markThreadRead = (threadKey: string, count: number): boolean => {
578+
if (this.syncdb == null) {
579+
return false;
580+
}
581+
const account_id = this.redux.getStore("account").get_account_id();
582+
if (!account_id || !Number.isFinite(count)) {
583+
return false;
584+
}
585+
const entry = this.getThreadRootDoc(threadKey);
586+
if (entry == null) {
587+
return false;
588+
}
589+
entry.doc[`read-${account_id}`] = count;
590+
this.syncdb.set(entry.doc);
591+
this.syncdb.commit();
592+
return true;
593+
};
594+
595+
private getThreadRootDoc = (
596+
threadKey: string,
597+
): { doc: any; message: ChatMessageTyped } | null => {
598+
if (this.store == null) {
599+
return null;
600+
}
601+
const messages = this.store.get("messages");
602+
if (messages == null) {
603+
return null;
604+
}
605+
const normalizedKey = toMsString(threadKey);
606+
const fallbackKey = `${parseInt(threadKey, 10)}`;
607+
const candidates = [normalizedKey, threadKey, fallbackKey];
608+
let message: ChatMessageTyped | undefined;
609+
for (const key of candidates) {
610+
if (!key) continue;
611+
message = messages.get(key);
612+
if (message != null) break;
613+
}
614+
if (message == null) {
615+
return null;
616+
}
617+
const dateField = message.get("date");
618+
const dateIso =
619+
dateField instanceof Date
620+
? dateField.toISOString()
621+
: typeof dateField === "string"
622+
? dateField
623+
: new Date(dateField).toISOString();
624+
if (!dateIso) {
625+
return null;
626+
}
627+
const doc = { ...message.toJS(), date: dateIso };
628+
return { doc, message };
629+
};
630+
484631
save_scroll_state = (position, height, offset): void => {
485632
if (height == 0) {
486633
// height == 0 means chat room is not rendered
@@ -1104,13 +1251,15 @@ export class ChatActions extends Actions<ChatState> {
11041251
};
11051252

11061253
setFragment = (date?) => {
1254+
let fragmentId;
11071255
if (!date) {
11081256
Fragment.clear();
1257+
fragmentId = "";
11091258
} else {
1110-
const fragmentId = toMsString(date);
1259+
fragmentId = toMsString(date);
11111260
Fragment.set({ chat: fragmentId });
1112-
this.frameTreeActions?.set_frame_data({ id: this.frameId, fragmentId });
11131261
}
1262+
this.frameTreeActions?.set_frame_data({ id: this.frameId, fragmentId });
11141263
};
11151264

11161265
setShowPreview = (showPreview) => {
@@ -1119,6 +1268,14 @@ export class ChatActions extends Actions<ChatState> {
11191268
showPreview,
11201269
});
11211270
};
1271+
1272+
setSelectedThread = (threadKey: string | null) => {
1273+
console.log("setSelectedThread", { threadKey });
1274+
this.frameTreeActions?.set_frame_data({
1275+
id: this.frameId,
1276+
selectedThreadKey: threadKey,
1277+
});
1278+
};
11221279
}
11231280

11241281
// We strip out any cased version of the string @chatgpt and also all mentions.

src/packages/frontend/chat/chat-log.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface Props {
5555
filterRecentH?;
5656
selectedHashtags;
5757
disableFilters?: boolean;
58+
selectedThread?: string;
5859
scrollToIndex?: null | number | undefined;
5960
// scrollToDate = string ms from epoch
6061
scrollToDate?: null | undefined | string;
@@ -74,12 +75,32 @@ export function ChatLog({
7475
filterRecentH,
7576
selectedHashtags: selectedHashtags0,
7677
disableFilters,
78+
selectedThread,
7779
scrollToIndex,
7880
scrollToDate,
7981
selectedDate,
8082
costEstimate,
8183
}: Props) {
82-
const messages = useRedux(["messages"], project_id, path) as ChatMessages;
84+
const storeMessages = useRedux(
85+
["messages"],
86+
project_id,
87+
path,
88+
) as ChatMessages;
89+
const singleThreadView = selectedThread != null;
90+
const messages = useMemo(() => {
91+
if (!selectedThread || storeMessages == null) {
92+
return storeMessages;
93+
}
94+
return storeMessages.filter((message) => {
95+
if (message == null) return false;
96+
const replyTo = message.get("reply_to");
97+
if (replyTo != null) {
98+
return `${new Date(replyTo).valueOf()}` === selectedThread;
99+
}
100+
const dateValue = message.get("date")?.valueOf();
101+
return dateValue != null ? `${dateValue}` === selectedThread : false;
102+
}) as ChatMessages;
103+
}, [storeMessages, selectedThread]);
83104
// see similar code in task list:
84105
const { selectedHashtags, selectedHashtagsSearch } = useMemo(() => {
85106
return getSelectedHashtagsSearch(selectedHashtags0);
@@ -102,6 +123,7 @@ export function ChatLog({
102123
search,
103124
account_id!,
104125
filterRecentH,
126+
singleThreadView,
105127
);
106128
// TODO: This is an ugly hack because I'm tired and need to finish this.
107129
// The right solution would be to move this filtering to the store.
@@ -114,7 +136,7 @@ export function ChatLog({
114136
);
115137
}, 1);
116138
return { dates, numFolded, numChildren };
117-
}, [messages, search, project_id, path, filterRecentH]);
139+
}, [messages, search, project_id, path, filterRecentH, singleThreadView]);
118140

119141
useEffect(() => {
120142
scrollToBottomRef?.current?.(true);
@@ -254,6 +276,7 @@ export function ChatLog({
254276
mode,
255277
selectedDate,
256278
numChildren,
279+
singleThreadView,
257280
}}
258281
/>
259282
<Composing
@@ -330,6 +353,7 @@ export function getSortedDates(
330353
search: string | undefined,
331354
account_id: string,
332355
filterRecentH?: number,
356+
disableFolding?: boolean,
333357
): {
334358
dates: string[];
335359
numFolded: number;
@@ -365,7 +389,7 @@ export function getSortedDates(
365389
if (message == null) continue;
366390

367391
// If we search for a message, we treat all threads as unfolded
368-
if (!search) {
392+
if (!disableFolding && !search) {
369393
const is_thread = isThread(message, numChildren);
370394
const is_folded = is_thread && isFolded(messages, message, account_id);
371395
const is_thread_body = is_thread && message.get("reply_to") != null;
@@ -494,6 +518,7 @@ export function MessageList({
494518
mode,
495519
selectedDate,
496520
numChildren,
521+
singleThreadView,
497522
}: {
498523
messages: ChatMessages;
499524
account_id: string;
@@ -510,6 +535,7 @@ export function MessageList({
510535
manualScrollRef?;
511536
selectedDate?: string;
512537
numChildren?: NumChildren;
538+
singleThreadView?: boolean;
513539
}) {
514540
const virtuosoHeightsRef = useRef<{ [index: number]: number }>({});
515541
const virtuosoScroll = useVirtuosoScrollHook({
@@ -546,7 +572,10 @@ export function MessageList({
546572
const is_thread = numChildren != null && isThread(message, numChildren);
547573
// optimization: only threads can be folded, so don't waste time
548574
// checking on folding state if it isn't a thread.
549-
const is_folded = is_thread && isFolded(messages, message, account_id);
575+
const is_folded =
576+
!singleThreadView &&
577+
is_thread &&
578+
isFolded(messages, message, account_id);
550579
const is_thread_body = is_thread && message.get("reply_to") != null;
551580
const h = virtuosoHeightsRef.current?.[index];
552581

@@ -594,9 +623,11 @@ export function MessageList({
594623
: undefined
595624
}
596625
allowReply={
626+
!singleThreadView &&
597627
messages.getIn([sortedDates[index + 1], "reply_to"]) == null
598628
}
599629
costEstimate={costEstimate}
630+
threadViewMode={singleThreadView}
600631
/>
601632
</DivTempHeight>
602633
</div>

0 commit comments

Comments
 (0)