Skip to content

Commit 2c5328b

Browse files
committed
feat: Implement group chats
1 parent ba531bd commit 2c5328b

28 files changed

Lines changed: 1515 additions & 173 deletions

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "YouTube Direct Messages",
3-
"version": "1.4.3",
3+
"version": "1.5.0",
44
"manifest_version": 3,
55
"description": "A Chrome extension to send direct messages on YouTube.",
66
"permissions": [

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "yt-dm-nl",
3-
"version": "1.4.3",
3+
"version": "1.5.0",
44
"description": "A Chrome extension to enable direct messaging functionality on YouTube.",
55
"private": true,
66
"scripts": {

src/core/panelController.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { authService } from '../services/authService';
2-
import { stateService, ViewType, IActiveChatContext } from '../services/stateService';
2+
import { stateService, ViewType } from '../services/stateService';
33
import { deeplinkService } from '../services/deeplinkService';
44
import { notificationService } from '../services/notificationService';
55
import { createToggleButton } from '../shared/components/toggleButton';
@@ -10,13 +10,17 @@ import { LoginView } from '../features/login/loginView';
1010
import { DialogsController } from '../features/dialogs/dialogsController';
1111
import { ChatController } from '../features/chat/chatController';
1212
import { SettingsController } from '../features/settings/settingsController';
13+
import { Chat } from '../types/chat';
14+
import { AddMemberController } from '../features/groups/addMemberController';
15+
import { EditGroupInfoController } from '../features/groups/editGroupInfoController';
16+
import { CreateGroupController } from '../features/groups/createGroupController';
1317

1418
export class PanelController {
1519
private panel: HTMLElement;
1620
private viewContainer: HTMLElement;
1721
private toggleButton: HTMLButtonElement;
1822

19-
private activeViewController: DialogsController | ChatController | SettingsController | null = null;
23+
private activeViewController: DialogsController | ChatController | SettingsController | AddMemberController | EditGroupInfoController | CreateGroupController | null = null;
2024

2125
constructor() {
2226
const { shell, viewContainer } = PanelView.createShell();
@@ -31,12 +35,13 @@ export class PanelController {
3135
}
3236

3337
private initializeServices(): void {
38+
stateService.initialize();
3439
authService.initialize();
3540
notificationService.initialize(this.toggleButton);
3641
deeplinkService.initialize({
37-
onTrigger: (chatContext: IActiveChatContext) => {
42+
onTrigger: (chat: Chat) => {
3843
if (!stateService.isPanelOpen()) this.togglePanel(true);
39-
stateService.openChat(chatContext);
44+
stateService.openChat(chat);
4045
}
4146
});
4247
}
@@ -92,6 +97,15 @@ export class PanelController {
9297
case ViewType.CHAT:
9398
this.activeViewController = new ChatController(this.viewContainer);
9499
break;
100+
case ViewType.ADD_MEMBER:
101+
this.activeViewController = new AddMemberController(this.viewContainer);
102+
break;
103+
case ViewType.EDIT_GROUP_INFO:
104+
this.activeViewController = new EditGroupInfoController(this.viewContainer);
105+
break;
106+
case ViewType.CREATE_GROUP:
107+
this.activeViewController = new CreateGroupController(this.viewContainer);
108+
break;
95109
case ViewType.SETTINGS_MAIN:
96110
case ViewType.SETTINGS_IGNORE_LIST:
97111
case ViewType.SETTINGS_APPEARANCE:

src/features/chat/chatController.ts

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,77 @@ import { ChatView, ChatViewProps } from './chatView';
77
import { Message } from '../../types/message';
88
import { updateReadTimestamp } from '../../shared/storage';
99
import { fetchYouTubeVideoDetails, parseVideoIdFromUrl } from '../../shared/utils/youtube';
10+
import { User } from '../../types/user';
11+
import { Chat, ChatType } from '../../types/chat';
12+
import { ViewType } from '../../services/stateService';
1013

1114
export class ChatController {
12-
private view: ChatView;
15+
private view: ChatView | null = null;
1316
private messages: Message[] = [];
1417
private oldestMessageDoc: QueryDocumentSnapshot | null = null;
1518
private isLoadingOlder = false;
1619
private listeners: Unsubscribe[] = [];
17-
private readonly chatId: string;
20+
private readonly chat: Chat;
21+
private partner: User | null = null;
1822

1923
constructor(private container: HTMLElement) {
2024
const context = stateService.activeChatContext;
2125
if (!context) {
2226
throw new Error("ChatController initialized without an active chat context.");
2327
}
24-
this.chatId = context.chatId;
25-
updateReadTimestamp(this.chatId);
28+
this.chat = context.chat;
29+
updateReadTimestamp(this.chat.id);
30+
31+
this.initializeController();
32+
}
33+
34+
private async initializeController(): Promise<void> {
35+
if (this.chat.type !== ChatType.GROUP) {
36+
const partnerUid = this.chat.participants.find(p => p !== authService.currentUser!.uid);
37+
if (partnerUid) {
38+
this.partner = await chatService.getUserProfile(partnerUid);
39+
}
40+
}
2641

2742
const props: ChatViewProps = {
28-
partner: context.partner,
43+
chat: this.chat,
44+
partner: this.partner,
2945
back: () => stateService.closeChat(),
3046
sendMessage: this.sendMessage.bind(this),
3147
shareVideo: this.shareVideo.bind(this),
3248
loadOlderMessages: this.loadOlderMessages.bind(this),
33-
ignoreUser: this.ignoreUser.bind(this),
3449
getVideoId: () => parseVideoIdFromUrl(window.location.href),
50+
ignoreUser: this.chat.type !== ChatType.GROUP && this.partner ? this.ignoreUser.bind(this, this.partner.uid) : undefined,
51+
leaveGroup: this.chat.type === ChatType.GROUP ? this.leaveGroup.bind(this) : undefined,
52+
addMember: this.chat.type === ChatType.GROUP ? this.addMember.bind(this) : undefined,
53+
editGroupInfo: this.chat.type === ChatType.GROUP ? this.editGroupInfo.bind(this) : undefined,
54+
deleteGroup: this.chat.type === ChatType.GROUP ? this.deleteGroup.bind(this) : undefined,
3555
};
3656

3757
this.view = new ChatView(this.container, props);
38-
this.fetchInitialMessages();
58+
await this.fetchInitialMessages();
59+
}
60+
61+
private editGroupInfo(): void {
62+
if (this.chat.type !== ChatType.GROUP) {
63+
throw new Error("Edit group info is only available for group chats.");
64+
}
65+
stateService.setView(ViewType.EDIT_GROUP_INFO);
66+
}
67+
68+
private addMember(): void {
69+
if (this.chat.type !== ChatType.GROUP) {
70+
throw new Error("Add member is only available for group chats.");
71+
}
72+
stateService.setView(ViewType.ADD_MEMBER);
3973
}
4074

4175
private async fetchInitialMessages(): Promise<void> {
42-
const { messages, oldestDoc } = await chatService.fetchInitialMessages(this.chatId);
76+
if (!this.view) return;
77+
const { messages, oldestDoc } = await chatService.fetchInitialMessages(this.chat.id);
4378
this.messages = messages;
4479
this.oldestMessageDoc = oldestDoc;
45-
this.view.renderMessages(this.messages, 'bottom');
80+
this.view.renderMessages(this.messages, this.chat.type, 'bottom');
4681
this.view.scrollToBottom();
4782
this.listenForNewMessages();
4883
}
@@ -52,15 +87,16 @@ export class ChatController {
5287
const lastTimestamp = (latestMessage && latestMessage.timestamp instanceof Timestamp) ? latestMessage.timestamp : null;
5388

5489
const newMessagesListener = chatService.listenToNewMessages(
55-
this.chatId,
90+
this.chat.id,
5691
lastTimestamp,
5792
(newMsgs) => {
93+
if (!this.view) return;
5894
const trulyNewMsgs = newMsgs.filter(msg => msg.from !== authService.currentUser?.uid);
59-
95+
6096
if (trulyNewMsgs.length > 0) {
6197
this.messages.push(...trulyNewMsgs);
62-
this.view.renderMessages(trulyNewMsgs, 'bottom', true);
63-
updateReadTimestamp(this.chatId);
98+
this.view.renderMessages(trulyNewMsgs, this.chat.type, 'bottom', true);
99+
updateReadTimestamp(this.chat.id);
64100
}
65101
}
66102
);
@@ -69,10 +105,11 @@ export class ChatController {
69105

70106
private async sendMessage(text: string): Promise<void> {
71107
this.addOptimisticMessage({ text });
72-
await chatService.addMessage(this.chatId, { text });
108+
await chatService.addMessage(this.chat.id, { text });
73109
}
74110

75111
private async shareVideo(includeTimestamp: boolean): Promise<void> {
112+
if (!this.view) return;
76113
const videoId = parseVideoIdFromUrl(window.location.href);
77114
if (!videoId) return;
78115

@@ -81,9 +118,9 @@ export class ChatController {
81118
const player = document.getElementById("movie_player") as any;
82119
const timestamp = includeTimestamp && player ? parseInt(player.getCurrentTime().toFixed(0)) : undefined;
83120
const videoData = await fetchYouTubeVideoDetails(videoId, timestamp);
84-
121+
85122
this.addOptimisticMessage({ video: videoData });
86-
await chatService.addMessage(this.chatId, { video: videoData });
123+
await chatService.addMessage(this.chat.id, { video: videoData });
87124
} catch (error) {
88125
console.error("Failed to share video:", error);
89126
alert("Could not share video.");
@@ -93,6 +130,7 @@ export class ChatController {
93130
}
94131

95132
private addOptimisticMessage(data: { text?: string; video?: any }): void {
133+
if (!this.view) return;
96134
const myUid = authService.currentUser!.uid;
97135
const optimisticMessage: Message = {
98136
id: `optimistic_${Date.now()}`,
@@ -101,23 +139,23 @@ export class ChatController {
101139
...data,
102140
};
103141
this.messages.push(optimisticMessage);
104-
this.view.renderMessages([optimisticMessage], 'bottom', true);
142+
this.view.renderMessages([optimisticMessage], this.chat.type, 'bottom', true);
105143
}
106-
144+
107145
private async loadOlderMessages(): Promise<void> {
108-
if (this.isLoadingOlder || !this.oldestMessageDoc) return;
146+
if (!this.view || this.isLoadingOlder || !this.oldestMessageDoc) return;
109147
this.isLoadingOlder = true;
110-
111-
const { messages: olderMessages, oldestDoc: newOldestDoc } = await chatService.fetchOlderMessages(this.chatId, this.oldestMessageDoc);
112-
148+
149+
const { messages: olderMessages, oldestDoc: newOldestDoc } = await chatService.fetchOlderMessages(this.chat.id, this.oldestMessageDoc);
150+
113151
if (olderMessages.length > 0) {
114152
this.messages = [...olderMessages, ...this.messages];
115153
this.oldestMessageDoc = newOldestDoc;
116-
this.view.renderMessages(olderMessages, 'top');
154+
this.view.renderMessages(olderMessages, this.chat.type, 'top');
117155
} else {
118156
this.oldestMessageDoc = null;
119157
}
120-
158+
121159
this.isLoadingOlder = false;
122160
}
123161

@@ -126,8 +164,38 @@ export class ChatController {
126164
stateService.closeChat();
127165
}
128166

167+
private async leaveGroup(): Promise<void> {
168+
if (confirm('Are you sure you want to leave this group?')) {
169+
try {
170+
await chatService.leaveChat(this.chat.id);
171+
stateService.setView(ViewType.DIALOGS);
172+
} catch (error) {
173+
console.error("Failed to leave group:", error);
174+
alert("Could not leave the group. Please try again.");
175+
}
176+
}
177+
}
178+
179+
private async deleteGroup(): Promise<void> {
180+
const userInput = prompt(`This action cannot be undone. All messages will be permanently deleted. To confirm, please type the group name "${this.chat.name}".`);
181+
182+
if (userInput === this.chat.name) {
183+
try {
184+
await chatService.deleteGroup(this.chat.id);
185+
stateService.setView(ViewType.DIALOGS);
186+
} catch (error) {
187+
console.error("Failed to delete group:", error);
188+
alert("Could not delete the group. Please try again.");
189+
}
190+
} else if (userInput !== null) {
191+
alert('Confirmation text did not match. Deletion canceled.');
192+
}
193+
}
194+
129195
public destroy(): void {
130-
this.view.destroy();
196+
this.view?.destroy();
197+
this.view = null;
131198
this.listeners.forEach(unsub => unsub());
199+
this.listeners = [];
132200
}
133201
}

0 commit comments

Comments
 (0)