diff --git a/src/lib/components/dashboard/notifications/NotificationDescription.svelte b/src/lib/components/dashboard/notifications/NotificationDescription.svelte index 17b0511a..07e3337b 100644 --- a/src/lib/components/dashboard/notifications/NotificationDescription.svelte +++ b/src/lib/components/dashboard/notifications/NotificationDescription.svelte @@ -77,6 +77,7 @@ width: 100%; -webkit-box-orient: vertical; color: variables.$grey-4; + hyphens: auto; -webkit-line-clamp: 2; word-wrap: break-word; diff --git a/src/lib/helpers/createNotificationData.ts b/src/lib/helpers/createNotificationData.ts index c3fe7ebf..7bb2ce13 100644 --- a/src/lib/helpers/createNotificationData.ts +++ b/src/lib/helpers/createNotificationData.ts @@ -1,4 +1,5 @@ import { page } from '$app/stores'; +import { getDiscussionUrl } from '$lib/helpers/searchNotificationHelper'; import { ClosedIssueIcon, CommitIcon, @@ -43,10 +44,11 @@ type PullRequestEvent = { type FetchOptions = Parameters[1]; export async function createNotificationData( - { id, repository, subject, unread: isUnread, updated_at, reason }: GithubNotification, + githubNotification: GithubNotification, savedNotifications: SavedNotifications, firstTime: boolean ): Promise { + const { id, repository, subject, unread: isUnread, updated_at, reason } = githubNotification; const previous = Array.isArray(savedNotifications) ? savedNotifications.find((n) => n.id === id) : undefined; @@ -267,14 +269,32 @@ export async function createNotificationData( break; } - case 'Discussion': + case 'Discussion': { + const data = await getDiscussionUrl(githubNotification).then(({ url, latestCommentEdge }) => { + if (!latestCommentEdge) { + return { + description: 'New activity on discussion' + }; + } + url += '#discussioncomment-' + latestCommentEdge.node.databaseId; + const author = latestCommentEdge.node.author; + return { + author: { + login: author.login, + avatar: author.avatarUrl, + bot: author.__typename === 'Bot' + }, + description: commentBodyToDescription(latestCommentEdge.node.bodyText), + url + }; + }); value = { ...common, - description: 'New activity on discussion', + ...data, icon: DiscussionIcon }; break; - + } case 'CheckSuite': { const splited = subject.title.split(' '); const workflowName = splited[0]; @@ -349,6 +369,10 @@ export async function createNotificationData( }; } +const commentBodyToDescription = (body: string) => { + return `*commented*: _${body.slice(0, 100)}${body.length > 100 ? '...' : ''}_`; +}; + async function getLatestComment( url: string, fetchOptions: FetchOptions @@ -361,8 +385,8 @@ async function getLatestComment( avatar: comment.user.avatar_url, bot: comment.user.type === 'Bot' }; - const body = removeMarkdownSymbols(comment.body).slice(0, 100); - const description = `*commented*: _${body}${body.length < 100 ? '...' : ''}_`; + const body = removeMarkdownSymbols(comment.body); + const description = commentBodyToDescription(body); return { author, description, time: comment.created_at, url: comment.html_url }; } diff --git a/src/lib/helpers/fetchGithub.ts b/src/lib/helpers/fetchGithub.ts index 5b890c3f..14c5238a 100644 --- a/src/lib/helpers/fetchGithub.ts +++ b/src/lib/helpers/fetchGithub.ts @@ -26,11 +26,10 @@ export async function fetchGithub(url: string, options?: Options): Pro cache: options?.noCache ? 'no-store' : undefined }); - if (options?.method) return undefined as T; + if (options?.method === 'PATCH') return undefined as T; if (response.ok) { - const data = await response.json(); - return data; + return await response.json(); } throw new Error(`${response.status}`); diff --git a/src/lib/helpers/searchNotificationHelper.ts b/src/lib/helpers/searchNotificationHelper.ts new file mode 100644 index 00000000..3ed9b0ae --- /dev/null +++ b/src/lib/helpers/searchNotificationHelper.ts @@ -0,0 +1,142 @@ +// source code from https://github.com/gitify-app/gitify/pull/538 +import { fetchGithub } from '$lib/helpers/fetchGithub'; +import type { GithubNotification } from '$lib/types'; + +export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED'; + +export interface GraphQLSearch { + data: { + search: { + edges: DiscussionEdge[]; + }; + }; +} + +export interface DiscussionEdge { + node: { + viewerSubscription: ViewerSubscription; + title: string; + url: string; + comments: { + edges: DiscussionCommentEdge[]; + }; + }; +} + +// https://docs.github.com/en/graphql/reference/interfaces#actor +export interface Actor { + login: string; + avatarUrl: string; + __typename: 'Bot' | 'EnterpriseUserAccount' | 'Mannequin' | 'Organization' | 'User'; +} + +export interface DiscussionCommentEdge { + node: { + databaseId: string | number; + createdAt: string; + author: Actor; + bodyText: string; + replies: { + edges: DiscussionSubCommentEdge[]; + }; + }; +} + +export interface DiscussionSubCommentEdge { + node: { + databaseId: string | number; + createdAt: string; + author: Actor; + bodyText: string; + }; +} + +const addHours = (date: string, hours: number) => + new Date(new Date(date).getTime() + hours * 36e5).toISOString(); + +const queryString = (repo: string, title: string, lastUpdated: string) => + `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`; + +export const getLatestDiscussionCommentEdge = (comments: DiscussionCommentEdge[]) => + comments + .flatMap((comment) => comment.node.replies.edges) + .concat([comments.at(-1) || ({} as DiscussionCommentEdge)]) + .reduce((a, b) => (a.node.createdAt > b.node.createdAt ? a : b)); + +export async function getDiscussionUrl(notification: GithubNotification): Promise<{ + url: string; + latestCommentEdge: DiscussionSubCommentEdge | undefined; +}> { + const response: GraphQLSearch = await fetchGithub('graphql', { + method: 'POST', + body: { + query: `{ + search(query:"${queryString( + notification.repository.full_name, + notification.subject.title, + notification.updated_at + )}" + type: DISCUSSION + first: 10 + ) { + edges { + node { + ... on Discussion { + viewerSubscription + title + url + comments(last: 100) { + edges { + node { + author { + login + avatarUrl + __typename + } + bodyText + databaseId + createdAt + replies(last: 1) { + edges { + node { + databaseId + createdAt + author { + login + avatarUrl + __typename + } + bodyText + } + } + } + } + } + } + } + } + } + } + }` + } + }); + + let edges = + response?.data?.search?.edges?.filter( + (edge) => edge.node.title === notification.subject.title + ) || []; + if (edges.length > 1) + edges = edges.filter((edge) => edge.node.viewerSubscription === 'SUBSCRIBED'); + + const comments = edges[0]?.node.comments.edges; + + let latestCommentEdge: DiscussionSubCommentEdge | undefined; + if (comments?.length) { + latestCommentEdge = getLatestDiscussionCommentEdge(comments); + } + + return { + url: edges[0]?.node.url, + latestCommentEdge + }; +}