Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/notifications #91

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NewBoardButton } from './NewBoardButton';
import { useParams, useSearchParams } from 'next/navigation';
import { BoardCard } from './board-card/Index';
import { Board, getAllBoards } from '@/actions/Board';
import useFcmToken, { subscribeToTopic } from '@/hooks/useFcmToken';

interface BoardListProps {
orgId: string;
Expand All @@ -20,6 +21,7 @@ export const BoardList = ({ orgId, query }: BoardListProps) => {
const [loading, setLoading] = useState(true);
const params = useParams();
const searchParams = useSearchParams();
const { token } = useFcmToken(null);

const fetchBoards = useCallback(async (userId: string, orgId: string) => {
try {
Expand Down Expand Up @@ -47,6 +49,12 @@ export const BoardList = ({ orgId, query }: BoardListProps) => {
setFilteredData(filteredBoards);
}, [data, searchParams]);

useEffect(() => {
filteredData.map((board) => {
subscribeToTopic(token!, board._id);
});
}, [token, filteredData]);

const handleBoardCreated = (newBoard: Board) => {
setData((prevData) => [newBoard, ...prevData]);
setFilteredData((prevFilteredData) => [newBoard, ...prevFilteredData]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ interface CanvasProps {
}

const Canvas: FC<CanvasProps> = ({ boardId }) => {
const [notificationSent, setNotificationSent] = useState(false);

const notifyBoardAuthor = async () => {
if (!notificationSent) {
setNotificationSent(true); // Блокируем дальнейшие отправки

const response = await fetch('/api/send-edit-notification', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topic: `board_${boardId}`,
}),
});

const data = await response.json();

if (!data.success) {
console.error('Ошибка отправки уведомления:', data.error);
setNotificationSent(false);
} else {
console.log('Уведомление успешно отправлено:', data.message);
}
}
};

const svgRef = useRef<SVGSVGElement>(null);
const [svgRect, setSvgRect] = useState<DOMRect | null>(null);
const layerIds = useStorage((root) => root.layerIds);
Expand All @@ -74,7 +101,9 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
const [editable, setEditable] = useState(false);

useEffect(() => {
if (membership) setEditable(true);
if (membership) {
setEditable(true);
}
}, [membership]);

const [camera, setCamera] = useState<Camera>({ x: 0, y: 0 });
Expand Down Expand Up @@ -371,6 +400,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
pencilDraft: [[point.x, point.y, pressure]],
penColor: lastUsedColor,
});

notifyBoardAuthor();
},
[lastUsedColor],
);
Expand Down Expand Up @@ -646,6 +677,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
newLayerIds.map((id) => liveLayerIds.push(id));
setMyPresence({ selection: newLayerIds });
setPasteCount((prev) => prev + 1);

notifyBoardAuthor();
}
},
[pasteCount, editable],
Expand Down Expand Up @@ -678,6 +711,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ fill: checked ? null : lastUsedColor });
}
});

notifyBoardAuthor();
},
[selection, lastUsedColor],
);
Expand All @@ -690,6 +725,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ lineWidth: width });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -702,6 +739,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ fontName: name });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -714,6 +753,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ fontSize: size });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -726,6 +767,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ textAlign: align });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -738,6 +781,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ textFormat: format });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -751,6 +796,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ x, y });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -764,6 +811,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
layer.update({ width, height });
}
});

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -786,6 +835,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
arr.length - 1 - (indices.length - 1 - i),
);
}

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -805,6 +856,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
for (let i = 0; i < indices.length; i++) {
liveLayerIds.move(indices[i], i);
}

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -819,6 +872,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
liveLayerIds.move(i - 1, i);
}
}

notifyBoardAuthor();
},
[selection],
);
Expand All @@ -833,6 +888,8 @@ const Canvas: FC<CanvasProps> = ({ boardId }) => {
liveLayerIds.move(i + 1, i);
}
}

notifyBoardAuthor();
},
[selection],
);
Expand Down
46 changes: 46 additions & 0 deletions frontend/app/api/send-edit-notification/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import admin, { ServiceAccount } from 'firebase-admin';
import { Message } from 'firebase-admin/messaging';
import { NextRequest, NextResponse } from 'next/server';

import generated from '@/service_key.json';

if (!admin.apps.length) {
const serviceAccount = generated as ServiceAccount;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}

export async function POST(request: NextRequest) {
const { topic } = await request.json();

const payload: Message = {
topic: topic,
notification: {
title: 'Your board has been modified',
body: 'A user has started editing your board!',
},
webpush: {
headers: {
Urgency: 'high',
},
fcmOptions: {
link: '/',
},
notification: {
tag: `unique-${topic}-edit`,
},
},
};

try {
await admin.messaging().send(payload);

return NextResponse.json({
success: true,
message: 'Notification sent!',
});
} catch (error) {
return NextResponse.json({ success: false, error });
}
}
40 changes: 40 additions & 0 deletions frontend/app/api/send-notification/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import admin, { ServiceAccount } from 'firebase-admin';
import { Message } from 'firebase-admin/messaging';
import { NextRequest, NextResponse } from 'next/server';

import generated from '@/service_key.json';

if (!admin.apps.length) {
const serviceAccount = generated as ServiceAccount;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}

export async function POST(request: NextRequest) {
const { token, title, message, link } = await request.json();

const payload: Message = {
token,
notification: {
title: title,
body: message,
},
webpush: link && {
fcmOptions: {
link,
},
},
};

try {
await admin.messaging().send(payload);

return NextResponse.json({
success: true,
message: 'Notification sent!',
});
} catch (error) {
return NextResponse.json({ success: false, error });
}
}
27 changes: 27 additions & 0 deletions frontend/app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as admin from 'firebase-admin';
import generated from '@/service_key.json';
import { ServiceAccount } from 'firebase-admin';
import { NextRequest, NextResponse } from 'next/server';

if (!admin.apps.length) {
const serviceAccount = generated as ServiceAccount;
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
}

export async function POST(request: NextRequest) {
const { token, topic } = await request.json();

try {
await admin.messaging().subscribeToTopic(token, topic);

return NextResponse.json({
success: true,
message: 'Subscribed to topic successfully',
});
} catch (error) {
console.error('Error subscribing to topic:', error);
return NextResponse.json({ success: false, error });
}
}
10 changes: 5 additions & 5 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ body,
--popover-foreground: 210 40% 98%;

--primary: 210 40% 98%;
--primary-foreground: hsl(210,40%,98%);
--primary-foreground: hsl(210, 40%, 98%);

--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
Expand Down Expand Up @@ -91,12 +91,12 @@ body,

@layer utilities {
.no-scrollbar {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}

.no-scrollbar::-webkit-scrollbar {
display: none; /* WebKit-based browsers (Chrome, Safari) */
display: none; /* WebKit-based browsers (Chrome, Safari) */
}
}
/* Ensure smooth scrolling for a better user experience */
Expand All @@ -119,7 +119,7 @@ html {
/* Удаление скроллбара */
.no-scrollbar {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
scrollbar-width: none; /* Firefox */
}

.no-scrollbar::-webkit-scrollbar {
Expand Down
40 changes: 40 additions & 0 deletions frontend/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getApp, getApps, initializeApp } from 'firebase/app';
import { getMessaging, getToken, isSupported } from 'firebase/messaging';
import { getFunctions, httpsCallable } from '@firebase/functions';

const firebaseConfig = {
apiKey: 'AIzaSyA1GD4xECInr01gFPsayQ7Kb1uAmqU8y7I',
authDomain: 'itmo-board.firebaseapp.com',
projectId: 'itmo-board',
storageBucket: 'itmo-board.firebasestorage.app',
messagingSenderId: '117334585829',
appId: '1:117334585829:web:e62f3f66f274b599037f0c',
measurementId: 'G-XM9Z4BDK8L',
};

const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();

const messaging = async () => {
const supported = await isSupported();
return supported ? getMessaging(app) : null;
};

export const fetchToken = async () => {
try {
const fcmMessaging = await messaging();
if (fcmMessaging) {
const token = await getToken(fcmMessaging, {
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_FCM_VAPID_KEY,
});
return token;
}
return null;
} catch (err) {
console.error('An error occurred while fetching the token:', err);
return null;
}
};

export { app, messaging };
export const functions = getFunctions(app);
export const sendNotification = httpsCallable(functions, 'sendNotification');
Loading
Loading