@rudderhq/server 0.2.9-canary.8 → 0.2.9
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.
- package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +2 -2
- package/dist/routes/messenger.d.ts.map +1 -1
- package/dist/routes/messenger.js +6 -1
- package/dist/routes/messenger.js.map +1 -1
- package/dist/services/messenger.d.ts +6 -1
- package/dist/services/messenger.d.ts.map +1 -1
- package/dist/services/messenger.js +403 -231
- package/dist/services/messenger.js.map +1 -1
- package/package.json +13 -13
- package/ui-dist/assets/{_basePickBy-CpPt_W_6.js → _basePickBy-DvVj7rIS.js} +1 -1
- package/ui-dist/assets/{_baseUniq-DhSRH2al.js → _baseUniq-7APKp32S.js} +1 -1
- package/ui-dist/assets/{arc-CTJsGqKU.js → arc-BOK9kp7K.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-q1l0BEmJ.js → architectureDiagram-2XIMDMQ5-CPWEJ5-Q.js} +1 -1
- package/ui-dist/assets/{blockDiagram-WCTKOSBZ-BJzhf8tv.js → blockDiagram-WCTKOSBZ-1p1VKW9q.js} +1 -1
- package/ui-dist/assets/{c4Diagram-IC4MRINW-Dsvl_y8w.js → c4Diagram-IC4MRINW-BwQGsEPe.js} +1 -1
- package/ui-dist/assets/channel-C-lsMG4t.js +1 -0
- package/ui-dist/assets/{chunk-4BX2VUAB-BMrY-qAF.js → chunk-4BX2VUAB-nW7FE3wg.js} +1 -1
- package/ui-dist/assets/{chunk-55IACEB6-BGx4hYzL.js → chunk-55IACEB6-dmAMmzcP.js} +1 -1
- package/ui-dist/assets/{chunk-FMBD7UC4-B19LpjrO.js → chunk-FMBD7UC4-7kIxdw00.js} +1 -1
- package/ui-dist/assets/{chunk-JSJVCQXG-BcWVRCSn.js → chunk-JSJVCQXG-Dh5JJrKH.js} +1 -1
- package/ui-dist/assets/{chunk-KX2RTZJC-DCHaLTgt.js → chunk-KX2RTZJC-DpU5afUI.js} +1 -1
- package/ui-dist/assets/{chunk-NQ4KR5QH-jz_GzT4N.js → chunk-NQ4KR5QH-PTSr9534.js} +1 -1
- package/ui-dist/assets/{chunk-QZHKN3VN-B2tyTuvA.js → chunk-QZHKN3VN-UPJs2alU.js} +1 -1
- package/ui-dist/assets/{chunk-WL4C6EOR-CZh-0Jyo.js → chunk-WL4C6EOR-BN_ENVee.js} +1 -1
- package/ui-dist/assets/classDiagram-VBA2DB6C-Cq3o3fUE.js +1 -0
- package/ui-dist/assets/classDiagram-v2-RAHNMMFH-Cq3o3fUE.js +1 -0
- package/ui-dist/assets/clone-B2MEXD1I.js +1 -0
- package/ui-dist/assets/{cose-bilkent-S5V4N54A-DWhfCZEc.js → cose-bilkent-S5V4N54A-CgScQJik.js} +1 -1
- package/ui-dist/assets/{dagre-KLK3FWXG-CVOylwDE.js → dagre-KLK3FWXG-BPckIGrY.js} +1 -1
- package/ui-dist/assets/{diagram-E7M64L7V-g7MrjlZ3.js → diagram-E7M64L7V-DuGPsP2X.js} +1 -1
- package/ui-dist/assets/{diagram-IFDJBPK2-BMReBEDi.js → diagram-IFDJBPK2-CJK8D8DE.js} +1 -1
- package/ui-dist/assets/{diagram-P4PSJMXO-D5ZQkK9R.js → diagram-P4PSJMXO-CQYGYERn.js} +1 -1
- package/ui-dist/assets/{erDiagram-INFDFZHY-CelY5qIl.js → erDiagram-INFDFZHY-B1t5SPtA.js} +1 -1
- package/ui-dist/assets/{flowDiagram-PKNHOUZH-D2S3D6LD.js → flowDiagram-PKNHOUZH-ol67OiZa.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-A5KZAMGK-VeaC-Osw.js → ganttDiagram-A5KZAMGK-C0IybBSk.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-Q0OwIdnh.js → gitGraphDiagram-K3NZZRJ6-CDI1KyGT.js} +1 -1
- package/ui-dist/assets/{graph-CTaTokeM.js → graph-DfbaTV8H.js} +1 -1
- package/ui-dist/assets/{index-SfR0fOjF.js → index-5VZyjxqG.js} +1 -1
- package/ui-dist/assets/{index-4snCfFLB.js → index-B4b9dJCZ.js} +1 -1
- package/ui-dist/assets/{index-Bi9jz_s1.js → index-BY0rvsQr.js} +1 -1
- package/ui-dist/assets/{index-CG8y9sHi.js → index-BbaZbFmu.js} +1 -1
- package/ui-dist/assets/{index-BwOqVTOX.js → index-BgWchnYK.js} +1 -1
- package/ui-dist/assets/{index-D_auIzT1.js → index-BkjRMCKV.js} +1 -1
- package/ui-dist/assets/{index-Bl6h3llH.js → index-Bp7FTWBa.js} +1 -1
- package/ui-dist/assets/{index-ffO4xnCv.js → index-BuQZXWs_.js} +1 -1
- package/ui-dist/assets/{index-B47wczcP.js → index-BwtxtbOn.js} +1 -1
- package/ui-dist/assets/{index-D1zkQEZD.js → index-C-8bU1a2.js} +1 -1
- package/ui-dist/assets/{index-DClnjK9T.js → index-C20tXHKr.js} +1 -1
- package/ui-dist/assets/{index-CZAxrPBR.js → index-CCoACxoq.js} +1 -1
- package/ui-dist/assets/{index-CCBwwSyP.js → index-CH1DDF-L.js} +1 -1
- package/ui-dist/assets/{index-CaHZbzOX.js → index-CQyjEt4k.js} +1 -1
- package/ui-dist/assets/{index-BGF3_9Wf.css → index-ChrDf7Tl.css} +1 -1
- package/ui-dist/assets/{index-BOOKoZaA.js → index-CiGhmrvv.js} +361 -356
- package/ui-dist/assets/{index-BVJS_4mL.js → index-CmWiZykS.js} +1 -1
- package/ui-dist/assets/{index-DYOk9J94.js → index-D37jXRBz.js} +1 -1
- package/ui-dist/assets/{index-Cjgq8ZJd.js → index-D7s_NX_D.js} +1 -1
- package/ui-dist/assets/{index-m5E0TZ2G.js → index-DNz3Dwni.js} +1 -1
- package/ui-dist/assets/{index-C6ZpX6D_.js → index-DYl1iDYY.js} +1 -1
- package/ui-dist/assets/{index-zzAQchXU.js → index-DdSgfd5C.js} +1 -1
- package/ui-dist/assets/{index-Bmio9tSL.js → index-DiLLpUas.js} +1 -1
- package/ui-dist/assets/{index-B3XzR0JX.js → index-yqabeuoc.js} +1 -1
- package/ui-dist/assets/{infoDiagram-LFFYTUFH-UuxdrO-4.js → infoDiagram-LFFYTUFH-Z3ElH_zP.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-DfewMpzx.js → ishikawaDiagram-PHBUUO56-CoatViQz.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-4ABVD52K-CyElHPHv.js → journeyDiagram-4ABVD52K-hhkgYz-z.js} +1 -1
- package/ui-dist/assets/{kanban-definition-K7BYSVSG-DV2eugya.js → kanban-definition-K7BYSVSG-Dkm2pUlW.js} +1 -1
- package/ui-dist/assets/{layout-DhyNWUBq.js → layout-Bf6FVhnF.js} +1 -1
- package/ui-dist/assets/{linear-CurqvqgD.js → linear-VR2Wuqio.js} +1 -1
- package/ui-dist/assets/{mermaid.core-DVY9qjLB.js → mermaid.core-BRZPHgF_.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-YRQLILUH-dlgLJK0i.js → mindmap-definition-YRQLILUH-RS2G-uEV.js} +1 -1
- package/ui-dist/assets/{pieDiagram-SKSYHLDU-OWc7n4zl.js → pieDiagram-SKSYHLDU-CiIiai5w.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-337W2JSQ-TrF2oXXY.js → quadrantDiagram-337W2JSQ-CW5xTip3.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-Cg5dg9aA.js → requirementDiagram-Z7DCOOCP-CGHA107c.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-QT66utjW.js → sankeyDiagram-WA2Y5GQK-B_tCRdvR.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-BZBj2U96.js → sequenceDiagram-2WXFIKYE-DkQVsPbO.js} +1 -1
- package/ui-dist/assets/{stateDiagram-RAJIS63D-DLC9Plgu.js → stateDiagram-RAJIS63D-B1FtfiY9.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-Bb4gItK8.js +1 -0
- package/ui-dist/assets/{timeline-definition-YZTLITO2-BKO4P0dR.js → timeline-definition-YZTLITO2-CmsH7kLP.js} +1 -1
- package/ui-dist/assets/{treemap-KZPCXAKY-CHu0FJn4.js → treemap-KZPCXAKY-Cf3Q5FzZ.js} +1 -1
- package/ui-dist/assets/{vennDiagram-LZ73GAT5-D5y_dnIJ.js → vennDiagram-LZ73GAT5-hNH0ehQ2.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-JWTSCODW-ir_gQkq3.js → xychartDiagram-JWTSCODW-CL69JgaO.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/channel-b-0sodEj.js +0 -1
- package/ui-dist/assets/classDiagram-VBA2DB6C-C-PuZxuu.js +0 -1
- package/ui-dist/assets/classDiagram-v2-RAHNMMFH-C-PuZxuu.js +0 -1
- package/ui-dist/assets/clone-CyENzMzj.js +0 -1
- package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-VuzuaYLO.js +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { and, desc, eq, gt, inArray,
|
|
2
|
-
import { activityLog, approvalComments, approvals, agents, authUsers, heartbeatRuns, issueComments, issues, joinRequests, messengerThreadUserStates, } from "@rudderhq/db";
|
|
1
|
+
import { and, desc, eq, gt, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import { activityLog, approvalComments, approvals, agents, authUsers, heartbeatRuns, issueComments, issueFollows, issues, joinRequests, messengerThreadUserStates, } from "@rudderhq/db";
|
|
3
3
|
import { formatMessengerPreview, formatMessengerTitle, } from "@rudderhq/shared";
|
|
4
|
-
import { issueService } from "./issues.js";
|
|
5
4
|
import { chatService } from "./chats.js";
|
|
6
5
|
import { budgetService } from "./budgets.js";
|
|
7
6
|
import { redactEventPayload } from "../redaction.js";
|
|
7
|
+
import { conflict } from "../errors.js";
|
|
8
8
|
const ISSUE_ACTIVITY_ACTIONS = [
|
|
9
9
|
"issue.updated",
|
|
10
10
|
"issue.approval_linked",
|
|
@@ -18,32 +18,8 @@ const ISSUE_ACTIVITY_ACTIONS = [
|
|
|
18
18
|
"heartbeat.retried",
|
|
19
19
|
];
|
|
20
20
|
const ACTIONABLE_APPROVAL_STATUSES = new Set(["pending"]);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
"issueIdentifier",
|
|
24
|
-
"_previous",
|
|
25
|
-
"source",
|
|
26
|
-
"reopened",
|
|
27
|
-
"reopenedFrom",
|
|
28
|
-
"normalizedFromStatus",
|
|
29
|
-
"normalizedReason",
|
|
30
|
-
]);
|
|
31
|
-
function issueUpdatedChangedKeys(details) {
|
|
32
|
-
if (!details)
|
|
33
|
-
return [];
|
|
34
|
-
return Object.keys(details).filter((key) => !ISSUE_UPDATE_METADATA_KEYS.has(key));
|
|
35
|
-
}
|
|
36
|
-
function isDescriptionOnlyIssueUpdate(activity) {
|
|
37
|
-
if (activity.action !== "issue.updated")
|
|
38
|
-
return false;
|
|
39
|
-
const changedKeys = issueUpdatedChangedKeys(activity.details);
|
|
40
|
-
return changedKeys.length === 1 && changedKeys[0] === "description";
|
|
41
|
-
}
|
|
42
|
-
function shouldNotifyIssueActivity(activity) {
|
|
43
|
-
if (isDescriptionOnlyIssueUpdate(activity))
|
|
44
|
-
return false;
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
21
|
+
const DEFAULT_ISSUE_THREAD_DETAIL_LIMIT = 50;
|
|
22
|
+
const MAX_ISSUE_THREAD_DETAIL_LIMIT = 100;
|
|
47
23
|
function truncate(value, max = 140) {
|
|
48
24
|
return formatMessengerPreview(value, { max });
|
|
49
25
|
}
|
|
@@ -75,6 +51,40 @@ function compareChronologicalActivity(a, b) {
|
|
|
75
51
|
return aTime - bTime;
|
|
76
52
|
return a.title.localeCompare(b.title);
|
|
77
53
|
}
|
|
54
|
+
function normalizeIssueThreadLimit(limit) {
|
|
55
|
+
if (typeof limit !== "number" || !Number.isFinite(limit))
|
|
56
|
+
return DEFAULT_ISSUE_THREAD_DETAIL_LIMIT;
|
|
57
|
+
return Math.min(MAX_ISSUE_THREAD_DETAIL_LIMIT, Math.max(1, Math.floor(limit)));
|
|
58
|
+
}
|
|
59
|
+
function encodeIssueThreadCursor(entry) {
|
|
60
|
+
const payload = {
|
|
61
|
+
activityAt: entry.latestActivityAt.toISOString(),
|
|
62
|
+
issueId: entry.issue.id,
|
|
63
|
+
};
|
|
64
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
65
|
+
}
|
|
66
|
+
function decodeIssueThreadCursor(cursor) {
|
|
67
|
+
if (!cursor)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
71
|
+
if (typeof decoded.activityAt !== "string" || Number.isNaN(new Date(decoded.activityAt).getTime()))
|
|
72
|
+
return null;
|
|
73
|
+
if (typeof decoded.issueId !== "string" || decoded.issueId.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
return { activityAt: decoded.activityAt, issueId: decoded.issueId };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function compareIssueThreadEntriesChronological(a, b) {
|
|
82
|
+
const aTime = a.latestActivityAt.getTime();
|
|
83
|
+
const bTime = b.latestActivityAt.getTime();
|
|
84
|
+
if (aTime !== bTime)
|
|
85
|
+
return aTime - bTime;
|
|
86
|
+
return a.issue.id.localeCompare(b.issue.id);
|
|
87
|
+
}
|
|
78
88
|
function threadKeyForChat(conversationId) {
|
|
79
89
|
return `chat:${conversationId}`;
|
|
80
90
|
}
|
|
@@ -181,9 +191,6 @@ function summarizeIssueActivity(activity, issue) {
|
|
|
181
191
|
return `${issue.title} updated`;
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
|
-
function isSelfAuthoredComment(comment, userId) {
|
|
185
|
-
return comment.authorUserId === userId;
|
|
186
|
-
}
|
|
187
194
|
function issueCommentAuthorLabel(comment, currentUserId) {
|
|
188
195
|
if (!comment)
|
|
189
196
|
return null;
|
|
@@ -196,9 +203,6 @@ function issueCommentAuthorLabel(comment, currentUserId) {
|
|
|
196
203
|
}
|
|
197
204
|
return "System";
|
|
198
205
|
}
|
|
199
|
-
function isSelfAuthoredActivity(activity, userId) {
|
|
200
|
-
return activity.actorType === "user" && activity.actorId === userId;
|
|
201
|
-
}
|
|
202
206
|
function summarizeApprovalPayload(approval) {
|
|
203
207
|
const payload = redactEventPayload(approval.payload);
|
|
204
208
|
if (!payload)
|
|
@@ -501,227 +505,395 @@ async function lastReadAtForThread(db, orgId, userId, threadKey, threadStates) {
|
|
|
501
505
|
return (await states).get(threadKey)?.lastReadAt ?? null;
|
|
502
506
|
}
|
|
503
507
|
export function messengerService(db) {
|
|
504
|
-
const issuesSvc = issueService(db);
|
|
505
508
|
const chatsSvc = chatService(db);
|
|
506
509
|
const budgetsSvc = budgetService(db);
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
510
|
+
const issueActionSqlList = sql.join(ISSUE_ACTIVITY_ACTIONS.map((action) => sql `${action}`), sql `, `);
|
|
511
|
+
function issueDescriptionOnlyActivitySql(alias) {
|
|
512
|
+
return sql `(
|
|
513
|
+
${sql.raw(`${alias}.action`)} = 'issue.updated'
|
|
514
|
+
and jsonb_typeof(${sql.raw(`${alias}.details`)}) = 'object'
|
|
515
|
+
and ${sql.raw(`${alias}.details`)} ? 'description'
|
|
516
|
+
and not exists (
|
|
517
|
+
select 1
|
|
518
|
+
from jsonb_object_keys(${sql.raw(`${alias}.details`)}) as detail_key(key)
|
|
519
|
+
where detail_key.key not in (
|
|
520
|
+
'description',
|
|
521
|
+
'identifier',
|
|
522
|
+
'issueIdentifier',
|
|
523
|
+
'_previous',
|
|
524
|
+
'source',
|
|
525
|
+
'reopened',
|
|
526
|
+
'reopenedFrom',
|
|
527
|
+
'normalizedFromStatus',
|
|
528
|
+
'normalizedReason'
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
)`;
|
|
532
|
+
}
|
|
533
|
+
function issueEntryRowsQuery(orgId, userId, tail = sql ``) {
|
|
534
|
+
const descriptionOnlyActivity = issueDescriptionOnlyActivitySql("activity_row");
|
|
535
|
+
const externalDescriptionOnlyActivity = issueDescriptionOnlyActivitySql("external_activity_row");
|
|
536
|
+
return sql `
|
|
537
|
+
with tracked_issue_ids as (
|
|
538
|
+
select ${issues.id} as id
|
|
539
|
+
from ${issues}
|
|
540
|
+
where ${issues.orgId} = ${orgId}
|
|
541
|
+
and ${issues.hiddenAt} is null
|
|
542
|
+
and ${issues.assigneeUserId} = ${userId}
|
|
543
|
+
union
|
|
544
|
+
select ${issues.id} as id
|
|
545
|
+
from ${issues}
|
|
546
|
+
where ${issues.orgId} = ${orgId}
|
|
547
|
+
and ${issues.hiddenAt} is null
|
|
548
|
+
and ${issues.createdByUserId} = ${userId}
|
|
549
|
+
union
|
|
550
|
+
select ${issues.id} as id
|
|
551
|
+
from ${issues}
|
|
552
|
+
where ${issues.orgId} = ${orgId}
|
|
553
|
+
and ${issues.hiddenAt} is null
|
|
554
|
+
and ${issues.reviewerUserId} = ${userId}
|
|
555
|
+
union
|
|
556
|
+
select ${issueFollows.issueId} as id
|
|
557
|
+
from ${issueFollows}
|
|
558
|
+
inner join ${issues} followed_issue
|
|
559
|
+
on followed_issue.id = ${issueFollows.issueId}
|
|
560
|
+
and followed_issue.org_id = ${issueFollows.orgId}
|
|
561
|
+
where ${issueFollows.orgId} = ${orgId}
|
|
562
|
+
and ${issueFollows.userId} = ${userId}
|
|
563
|
+
and followed_issue.hidden_at is null
|
|
564
|
+
),
|
|
565
|
+
issue_entries as (
|
|
566
|
+
select
|
|
567
|
+
issue_row.id as id,
|
|
568
|
+
issue_row.title as title,
|
|
569
|
+
issue_row.status as status,
|
|
570
|
+
issue_row.priority as priority,
|
|
571
|
+
issue_row.assignee_user_id as "assigneeUserId",
|
|
572
|
+
issue_row.reviewer_user_id as "reviewerUserId",
|
|
573
|
+
issue_row.created_by_user_id as "createdByUserId",
|
|
574
|
+
issue_row.identifier as identifier,
|
|
575
|
+
issue_row.updated_at as "updatedAt",
|
|
576
|
+
exists (
|
|
577
|
+
select 1
|
|
578
|
+
from ${issueFollows} follow_row
|
|
579
|
+
where follow_row.org_id = ${orgId}
|
|
580
|
+
and follow_row.user_id = ${userId}
|
|
581
|
+
and follow_row.issue_id = issue_row.id
|
|
582
|
+
) as followed,
|
|
583
|
+
(issue_row.assignee_user_id = ${userId}) as assigned,
|
|
584
|
+
greatest(
|
|
585
|
+
issue_row.updated_at,
|
|
586
|
+
coalesce(latest_external_comment.created_at, issue_row.updated_at),
|
|
587
|
+
coalesce(latest_activity.created_at, issue_row.updated_at)
|
|
588
|
+
) as "latestActivityAt",
|
|
589
|
+
latest_activity.id as "latestActivityId",
|
|
590
|
+
latest_activity.action as "latestActivityAction",
|
|
591
|
+
latest_activity.actor_type as "latestActivityActorType",
|
|
592
|
+
latest_activity.actor_id as "latestActivityActorId",
|
|
593
|
+
latest_activity.details as "latestActivityDetails",
|
|
594
|
+
latest_activity.created_at as "latestActivityCreatedAt",
|
|
595
|
+
latest_activity.run_id as "latestActivityRunId",
|
|
596
|
+
case
|
|
597
|
+
when latest_external_comment.created_at is not null
|
|
598
|
+
and (latest_external_activity.created_at is null or latest_external_comment.created_at >= latest_external_activity.created_at)
|
|
599
|
+
then latest_external_comment.created_at
|
|
600
|
+
when latest_external_activity.created_at is not null
|
|
601
|
+
then latest_external_activity.created_at
|
|
602
|
+
when latest_activity.id is null
|
|
603
|
+
and (
|
|
604
|
+
latest_suppressed_activity.created_at is null
|
|
605
|
+
or latest_suppressed_activity.created_at < issue_row.updated_at - interval '5 seconds'
|
|
606
|
+
)
|
|
607
|
+
and (
|
|
608
|
+
issue_row.assignee_user_id = ${userId}
|
|
609
|
+
or (issue_row.reviewer_user_id = ${userId} and issue_row.status = 'in_review')
|
|
610
|
+
)
|
|
611
|
+
then issue_row.updated_at
|
|
612
|
+
else null
|
|
613
|
+
end as "attentionActivityAt",
|
|
614
|
+
latest_external_comment.body as "latestExternalCommentBody",
|
|
615
|
+
latest_external_comment.created_at as "latestExternalCommentCreatedAt",
|
|
616
|
+
latest_external_activity.id as "latestExternalActivityId",
|
|
617
|
+
latest_external_activity.action as "latestExternalActivityAction",
|
|
618
|
+
latest_external_activity.actor_type as "latestExternalActivityActorType",
|
|
619
|
+
latest_external_activity.actor_id as "latestExternalActivityActorId",
|
|
620
|
+
latest_external_activity.details as "latestExternalActivityDetails",
|
|
621
|
+
latest_external_activity.created_at as "latestExternalActivityCreatedAt",
|
|
622
|
+
latest_external_activity.run_id as "latestExternalActivityRunId"
|
|
623
|
+
from tracked_issue_ids
|
|
624
|
+
inner join ${issues} issue_row on issue_row.id = tracked_issue_ids.id
|
|
625
|
+
left join lateral (
|
|
626
|
+
select
|
|
627
|
+
comment_row.body,
|
|
628
|
+
comment_row.created_at
|
|
629
|
+
from ${issueComments} comment_row
|
|
630
|
+
where comment_row.org_id = ${orgId}
|
|
631
|
+
and comment_row.issue_id = issue_row.id
|
|
632
|
+
and (comment_row.author_user_id is null or comment_row.author_user_id <> ${userId})
|
|
633
|
+
order by comment_row.created_at desc, comment_row.id desc
|
|
634
|
+
limit 1
|
|
635
|
+
) latest_external_comment on true
|
|
636
|
+
left join lateral (
|
|
637
|
+
select
|
|
638
|
+
activity_row.id,
|
|
639
|
+
activity_row.action,
|
|
640
|
+
activity_row.actor_type,
|
|
641
|
+
activity_row.actor_id,
|
|
642
|
+
activity_row.details,
|
|
643
|
+
activity_row.created_at,
|
|
644
|
+
activity_row.run_id
|
|
645
|
+
from ${activityLog} activity_row
|
|
646
|
+
where activity_row.org_id = ${orgId}
|
|
647
|
+
and activity_row.entity_type = 'issue'
|
|
648
|
+
and activity_row.entity_id = issue_row.id::text
|
|
649
|
+
and activity_row.action in (${issueActionSqlList})
|
|
650
|
+
and not ${descriptionOnlyActivity}
|
|
651
|
+
order by activity_row.created_at desc, activity_row.id desc
|
|
652
|
+
limit 1
|
|
653
|
+
) latest_activity on true
|
|
654
|
+
left join lateral (
|
|
655
|
+
select
|
|
656
|
+
external_activity_row.id,
|
|
657
|
+
external_activity_row.action,
|
|
658
|
+
external_activity_row.actor_type,
|
|
659
|
+
external_activity_row.actor_id,
|
|
660
|
+
external_activity_row.details,
|
|
661
|
+
external_activity_row.created_at,
|
|
662
|
+
external_activity_row.run_id
|
|
663
|
+
from ${activityLog} external_activity_row
|
|
664
|
+
where external_activity_row.org_id = ${orgId}
|
|
665
|
+
and external_activity_row.entity_type = 'issue'
|
|
666
|
+
and external_activity_row.entity_id = issue_row.id::text
|
|
667
|
+
and external_activity_row.action in (${issueActionSqlList})
|
|
668
|
+
and not ${externalDescriptionOnlyActivity}
|
|
669
|
+
and (external_activity_row.actor_type <> 'user' or external_activity_row.actor_id <> ${userId})
|
|
670
|
+
order by external_activity_row.created_at desc, external_activity_row.id desc
|
|
671
|
+
limit 1
|
|
672
|
+
) latest_external_activity on true
|
|
673
|
+
left join lateral (
|
|
674
|
+
select suppressed_activity_row.created_at
|
|
675
|
+
from ${activityLog} suppressed_activity_row
|
|
676
|
+
where suppressed_activity_row.org_id = ${orgId}
|
|
677
|
+
and suppressed_activity_row.entity_type = 'issue'
|
|
678
|
+
and suppressed_activity_row.entity_id = issue_row.id::text
|
|
679
|
+
and suppressed_activity_row.action in (${issueActionSqlList})
|
|
680
|
+
and ${issueDescriptionOnlyActivitySql("suppressed_activity_row")}
|
|
681
|
+
order by suppressed_activity_row.created_at desc, suppressed_activity_row.id desc
|
|
682
|
+
limit 1
|
|
683
|
+
) latest_suppressed_activity on true
|
|
684
|
+
)
|
|
685
|
+
select *
|
|
686
|
+
from issue_entries
|
|
687
|
+
${tail}
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
async function loadLatestIssueCommentsForDisplay(orgId, issueIds) {
|
|
691
|
+
if (issueIds.length === 0)
|
|
692
|
+
return [];
|
|
693
|
+
return (await db
|
|
694
|
+
.selectDistinctOn([issueComments.issueId], {
|
|
695
|
+
id: issueComments.id,
|
|
696
|
+
issueId: issueComments.issueId,
|
|
697
|
+
body: issueComments.body,
|
|
698
|
+
authorAgentId: issueComments.authorAgentId,
|
|
699
|
+
authorUserId: issueComments.authorUserId,
|
|
700
|
+
authorAgentName: agents.name,
|
|
701
|
+
authorUserName: authUsers.name,
|
|
702
|
+
createdAt: issueComments.createdAt,
|
|
703
|
+
})
|
|
704
|
+
.from(issueComments)
|
|
705
|
+
.leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
|
|
706
|
+
.leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
|
|
707
|
+
.where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds)))
|
|
708
|
+
.orderBy(issueComments.issueId, desc(issueComments.createdAt), desc(issueComments.id)));
|
|
709
|
+
}
|
|
710
|
+
function issueThreadEntryFromRow(row, userId) {
|
|
711
|
+
const updatedAt = normalizeDate(row.updatedAt) ?? new Date(row.updatedAt);
|
|
712
|
+
const latestActivityAt = normalizeDate(row.latestActivityAt) ?? updatedAt;
|
|
713
|
+
const latestActivityCreatedAt = normalizeDate(row.latestActivityCreatedAt);
|
|
714
|
+
const latestExternalActivityCreatedAt = normalizeDate(row.latestExternalActivityCreatedAt);
|
|
715
|
+
const issue = {
|
|
716
|
+
id: row.id,
|
|
717
|
+
title: row.title,
|
|
718
|
+
status: row.status,
|
|
719
|
+
priority: row.priority,
|
|
720
|
+
assigneeUserId: row.assigneeUserId,
|
|
721
|
+
reviewerUserId: row.reviewerUserId,
|
|
722
|
+
createdByUserId: row.createdByUserId,
|
|
723
|
+
identifier: row.identifier,
|
|
724
|
+
updatedAt,
|
|
725
|
+
followed: row.followed,
|
|
726
|
+
assigned: row.assigned,
|
|
727
|
+
};
|
|
728
|
+
const latestActivity = row.latestActivityId && row.latestActivityAction && row.latestActivityActorType && row.latestActivityActorId && latestActivityCreatedAt
|
|
729
|
+
? {
|
|
730
|
+
id: row.latestActivityId,
|
|
731
|
+
action: row.latestActivityAction,
|
|
732
|
+
entityId: row.id,
|
|
733
|
+
actorType: row.latestActivityActorType,
|
|
734
|
+
actorId: row.latestActivityActorId,
|
|
735
|
+
details: row.latestActivityDetails,
|
|
736
|
+
createdAt: latestActivityCreatedAt,
|
|
737
|
+
runId: row.latestActivityRunId,
|
|
738
|
+
}
|
|
739
|
+
: null;
|
|
740
|
+
const latestExternalActivity = row.latestExternalActivityId &&
|
|
741
|
+
row.latestExternalActivityAction &&
|
|
742
|
+
row.latestExternalActivityActorType &&
|
|
743
|
+
row.latestExternalActivityActorId &&
|
|
744
|
+
latestExternalActivityCreatedAt
|
|
745
|
+
? {
|
|
746
|
+
id: row.latestExternalActivityId,
|
|
747
|
+
action: row.latestExternalActivityAction,
|
|
748
|
+
entityId: row.id,
|
|
749
|
+
actorType: row.latestExternalActivityActorType,
|
|
750
|
+
actorId: row.latestExternalActivityActorId,
|
|
751
|
+
details: row.latestExternalActivityDetails,
|
|
752
|
+
createdAt: latestExternalActivityCreatedAt,
|
|
753
|
+
runId: row.latestExternalActivityRunId,
|
|
754
|
+
}
|
|
755
|
+
: null;
|
|
756
|
+
const latestExternalCommentAt = normalizeDate(row.latestExternalCommentCreatedAt);
|
|
757
|
+
const attentionActivityAt = normalizeDate(row.attentionActivityAt);
|
|
758
|
+
const latestExternalActivityAt = normalizeDate(latestExternalActivity?.createdAt ?? null);
|
|
759
|
+
const attentionPreview = latestExternalCommentAt &&
|
|
760
|
+
(!latestExternalActivityAt || latestExternalCommentAt.getTime() >= latestExternalActivityAt.getTime())
|
|
761
|
+
? truncate(row.latestExternalCommentBody)
|
|
762
|
+
: latestExternalActivity
|
|
763
|
+
? summarizeIssueActivity(latestExternalActivity, issue)
|
|
764
|
+
: null;
|
|
765
|
+
const fallbackPreview = attentionPreview
|
|
766
|
+
?? (attentionActivityAt
|
|
767
|
+
? issueBodyFromSnapshot(issue, null, row.followed, row.createdByUserId === userId, row.assigneeUserId === userId, row.reviewerUserId === userId && row.status === "in_review")
|
|
768
|
+
: null);
|
|
769
|
+
return {
|
|
770
|
+
issue,
|
|
771
|
+
latestActivityAt,
|
|
772
|
+
latestActivity,
|
|
773
|
+
attentionActivityAt,
|
|
774
|
+
attentionPreview: attentionActivityAt ? issueThreadPreview(issue, fallbackPreview) : null,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function loadIssueThreadStats(orgId, userId, lastReadAt) {
|
|
778
|
+
const lastReadAtIso = lastReadAt?.toISOString() ?? null;
|
|
779
|
+
const rows = (await db.execute(sql `
|
|
780
|
+
select
|
|
781
|
+
count(*)::int as "itemCount",
|
|
782
|
+
count(*) filter (
|
|
783
|
+
where "attentionActivityAt" is not null
|
|
784
|
+
and (${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz)
|
|
785
|
+
)::int as "unreadCount",
|
|
786
|
+
max("attentionActivityAt") filter (
|
|
787
|
+
where ${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz
|
|
788
|
+
) as "latestActivityAt"
|
|
789
|
+
from (${issueEntryRowsQuery(orgId, userId)}) issue_entry_stats
|
|
790
|
+
`));
|
|
791
|
+
const row = rows[0];
|
|
792
|
+
return row
|
|
793
|
+
? {
|
|
794
|
+
itemCount: Number(row.itemCount),
|
|
795
|
+
unreadCount: Number(row.unreadCount),
|
|
796
|
+
latestActivityAt: normalizeDate(row.latestActivityAt),
|
|
797
|
+
}
|
|
798
|
+
: { itemCount: 0, unreadCount: 0, latestActivityAt: null };
|
|
799
|
+
}
|
|
800
|
+
async function loadLatestUnreadIssueEntry(orgId, userId, lastReadAt) {
|
|
801
|
+
const lastReadAtIso = lastReadAt?.toISOString() ?? null;
|
|
802
|
+
const rows = (await db.execute(issueEntryRowsQuery(orgId, userId, sql `
|
|
803
|
+
where "attentionActivityAt" is not null
|
|
804
|
+
and (${lastReadAtIso}::timestamptz is null or "attentionActivityAt" > ${lastReadAtIso}::timestamptz)
|
|
805
|
+
order by "attentionActivityAt" desc, id asc
|
|
806
|
+
limit 1
|
|
807
|
+
`)));
|
|
808
|
+
return rows[0] ? issueThreadEntryFromRow(rows[0], userId) : null;
|
|
809
|
+
}
|
|
810
|
+
async function loadIssueDetailEntries(orgId, userId, limit, cursor) {
|
|
811
|
+
const cursorActivityAt = cursor ? new Date(cursor.activityAt).toISOString() : null;
|
|
812
|
+
const rows = (await db.execute(issueEntryRowsQuery(orgId, userId, sql `
|
|
813
|
+
${cursor
|
|
814
|
+
? sql `
|
|
815
|
+
where (
|
|
816
|
+
"latestActivityAt" < ${cursorActivityAt}::timestamptz
|
|
817
|
+
or ("latestActivityAt" = ${cursorActivityAt}::timestamptz and id > ${cursor.issueId})
|
|
818
|
+
)
|
|
819
|
+
`
|
|
820
|
+
: sql ``}
|
|
821
|
+
order by "latestActivityAt" desc, id asc
|
|
822
|
+
limit ${limit + 1}
|
|
823
|
+
`)));
|
|
824
|
+
return rows.map((row) => issueThreadEntryFromRow(row, userId));
|
|
544
825
|
}
|
|
545
826
|
async function loadIssueData(orgId, userId, threadStates, options) {
|
|
546
827
|
const lastReadAtPromise = lastReadAtForThread(db, orgId, userId, "issues", threadStates);
|
|
547
|
-
const issuesUniverse = await loadIssueUniverse(orgId, userId);
|
|
548
|
-
const issueIds = issuesUniverse.map((row) => row.id);
|
|
549
828
|
const lastReadAt = await lastReadAtPromise;
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
? db
|
|
555
|
-
.select({
|
|
556
|
-
id: issueComments.id,
|
|
557
|
-
issueId: issueComments.issueId,
|
|
558
|
-
body: issueComments.body,
|
|
559
|
-
authorAgentId: issueComments.authorAgentId,
|
|
560
|
-
authorUserId: issueComments.authorUserId,
|
|
561
|
-
authorAgentName: agents.name,
|
|
562
|
-
authorUserName: authUsers.name,
|
|
563
|
-
createdAt: issueComments.createdAt,
|
|
564
|
-
})
|
|
565
|
-
.from(issueComments)
|
|
566
|
-
.leftJoin(agents, eq(issueComments.authorAgentId, agents.id))
|
|
567
|
-
.leftJoin(authUsers, eq(issueComments.authorUserId, authUsers.id))
|
|
568
|
-
.where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds)))
|
|
569
|
-
.orderBy(desc(issueComments.createdAt))
|
|
570
|
-
: db
|
|
571
|
-
.select({
|
|
572
|
-
id: issueComments.id,
|
|
573
|
-
issueId: issueComments.issueId,
|
|
574
|
-
body: issueComments.body,
|
|
575
|
-
authorAgentId: issueComments.authorAgentId,
|
|
576
|
-
authorUserId: issueComments.authorUserId,
|
|
577
|
-
authorAgentName: sql `null`,
|
|
578
|
-
authorUserName: sql `null`,
|
|
579
|
-
createdAt: issueComments.createdAt,
|
|
580
|
-
})
|
|
581
|
-
.from(issueComments)
|
|
582
|
-
.where(and(eq(issueComments.orgId, orgId), inArray(issueComments.issueId, issueIds), sql `(${issueComments.authorUserId} is null or ${issueComments.authorUserId} <> ${userId})`))
|
|
583
|
-
.orderBy(desc(issueComments.createdAt)),
|
|
584
|
-
issueIds.length === 0
|
|
585
|
-
? Promise.resolve([])
|
|
586
|
-
: db
|
|
587
|
-
.select({
|
|
588
|
-
id: activityLog.id,
|
|
589
|
-
action: activityLog.action,
|
|
590
|
-
entityId: activityLog.entityId,
|
|
591
|
-
actorType: activityLog.actorType,
|
|
592
|
-
actorId: activityLog.actorId,
|
|
593
|
-
details: activityLog.details,
|
|
594
|
-
createdAt: activityLog.createdAt,
|
|
595
|
-
runId: activityLog.runId,
|
|
596
|
-
})
|
|
597
|
-
.from(activityLog)
|
|
598
|
-
.where(and(eq(activityLog.orgId, orgId), eq(activityLog.entityType, "issue"), inArray(activityLog.entityId, issueIds), inArray(activityLog.action, ISSUE_ACTIVITY_ACTIONS)))
|
|
599
|
-
.orderBy(desc(activityLog.createdAt)),
|
|
600
|
-
]);
|
|
601
|
-
const latestCommentByIssue = new Map();
|
|
602
|
-
const latestExternalCommentByIssue = new Map();
|
|
603
|
-
for (const row of commentRows) {
|
|
604
|
-
if (options.includeDetail && !latestCommentByIssue.has(row.issueId)) {
|
|
605
|
-
latestCommentByIssue.set(row.issueId, row);
|
|
606
|
-
}
|
|
607
|
-
if (!isSelfAuthoredComment(row, userId) && !latestExternalCommentByIssue.has(row.issueId)) {
|
|
608
|
-
latestExternalCommentByIssue.set(row.issueId, row);
|
|
609
|
-
}
|
|
829
|
+
const detailLimit = normalizeIssueThreadLimit(options.limit);
|
|
830
|
+
const decodedCursor = decodeIssueThreadCursor(options.cursor);
|
|
831
|
+
if (options.cursor && !decodedCursor) {
|
|
832
|
+
throw conflict("Messenger issues cursor is invalid or expired");
|
|
610
833
|
}
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
latestExternalActivityByIssue.set(row.entityId, row);
|
|
626
|
-
}
|
|
834
|
+
const [stats, latestAttentionEntry, detailEntries] = await Promise.all([
|
|
835
|
+
loadIssueThreadStats(orgId, userId, lastReadAt),
|
|
836
|
+
loadLatestUnreadIssueEntry(orgId, userId, lastReadAt),
|
|
837
|
+
options.includeDetail
|
|
838
|
+
? loadIssueDetailEntries(orgId, userId, detailLimit, decodedCursor)
|
|
839
|
+
: Promise.resolve([]),
|
|
840
|
+
]);
|
|
841
|
+
const hasMoreDetailEntries = options.includeDetail && detailEntries.length > detailLimit;
|
|
842
|
+
const pageEntries = hasMoreDetailEntries ? detailEntries.slice(0, detailLimit) : detailEntries;
|
|
843
|
+
const cursorEntry = hasMoreDetailEntries ? pageEntries.at(-1) ?? null : null;
|
|
844
|
+
const latestDisplayCommentRows = await loadLatestIssueCommentsForDisplay(orgId, pageEntries.map((entry) => entry.issue.id));
|
|
845
|
+
const latestDisplayCommentByIssue = new Map();
|
|
846
|
+
for (const row of latestDisplayCommentRows) {
|
|
847
|
+
latestDisplayCommentByIssue.set(row.issueId, row);
|
|
627
848
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
: latestActivity
|
|
641
|
-
? summarizeIssueActivity(latestActivity, issue)
|
|
849
|
+
const chronologicalItems = pageEntries
|
|
850
|
+
.sort(compareIssueThreadEntriesChronological)
|
|
851
|
+
.map((entry) => {
|
|
852
|
+
const latestDisplayComment = latestDisplayCommentByIssue.get(entry.issue.id) ?? null;
|
|
853
|
+
const latestDisplayCommentAt = normalizeDate(latestDisplayComment?.createdAt ?? null);
|
|
854
|
+
const latestSourceIsComment = Boolean(latestDisplayCommentAt &&
|
|
855
|
+
(!entry.latestActivity?.createdAt || latestDisplayCommentAt.getTime() >= new Date(entry.latestActivity.createdAt).getTime()));
|
|
856
|
+
const sourceComment = latestSourceIsComment ? latestDisplayComment : null;
|
|
857
|
+
const latestPreview = sourceComment
|
|
858
|
+
? truncate(sourceComment.body)
|
|
859
|
+
: entry.latestActivity
|
|
860
|
+
? summarizeIssueActivity(entry.latestActivity, entry.issue)
|
|
642
861
|
: null;
|
|
643
|
-
const statusChangeActivity =
|
|
644
|
-
? (issueStatusActivityMatchesSourceComment(latestActivity,
|
|
645
|
-
: latestActivity;
|
|
646
|
-
|
|
647
|
-
const latestExternalCommentAt = normalizeDate(latestExternalComment?.createdAt ?? null);
|
|
648
|
-
const latestSuppressedActivityAt = normalizeDate(latestSuppressedActivityByIssue.get(issue.id)?.createdAt ?? null);
|
|
649
|
-
const issueUpdatedAt = normalizeDate(issue.updatedAt);
|
|
650
|
-
const suppressedActivityMatchesIssueUpdate = Boolean(latestSuppressedActivityAt &&
|
|
651
|
-
issueUpdatedAt &&
|
|
652
|
-
latestSuppressedActivityAt.getTime() >= issueUpdatedAt.getTime() - 5_000);
|
|
653
|
-
const fallbackAssignedActivityAt = issue.assigneeUserId === userId && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
|
|
654
|
-
? issueUpdatedAt
|
|
655
|
-
: null;
|
|
656
|
-
const fallbackReviewerActivityAt = issue.reviewerUserId === userId && issue.status === "in_review" && !latestActivityByIssue.has(issue.id) && !suppressedActivityMatchesIssueUpdate
|
|
657
|
-
? issueUpdatedAt
|
|
658
|
-
: null;
|
|
659
|
-
const attentionActivityAt = maxDate(latestExternalCommentAt, latestExternalActivity?.createdAt, fallbackAssignedActivityAt, fallbackReviewerActivityAt);
|
|
660
|
-
const attentionPreview = latestExternalCommentAt &&
|
|
661
|
-
(!latestExternalActivity?.createdAt || latestExternalCommentAt.getTime() >= new Date(latestExternalActivity.createdAt).getTime())
|
|
662
|
-
? truncate(latestExternalComment?.body)
|
|
663
|
-
: latestExternalActivity
|
|
664
|
-
? summarizeIssueActivity(latestExternalActivity, issue)
|
|
665
|
-
: fallbackAssignedActivityAt || fallbackReviewerActivityAt
|
|
666
|
-
? issueBodyFromSnapshot(issue, null, issue.followed, issue.createdByUserId === userId, issue.assigneeUserId === userId, issue.reviewerUserId === userId && issue.status === "in_review")
|
|
667
|
-
: null;
|
|
668
|
-
const summaryPreview = attentionActivityAt ? issueThreadPreview(issue, attentionPreview) : null;
|
|
669
|
-
return {
|
|
670
|
-
issue,
|
|
671
|
-
item: options.includeDetail
|
|
672
|
-
? issueCard(issue, userId, issue.followed, latestPreview, latestActivityAt ?? issue.updatedAt, latestSourceIsComment ? latestVisibleComment : null, statusChangeActivity)
|
|
673
|
-
: null,
|
|
674
|
-
attentionActivityAt,
|
|
675
|
-
attentionPreview: summaryPreview,
|
|
676
|
-
};
|
|
677
|
-
});
|
|
678
|
-
const latestFirstEntries = [...unsortedEntries].sort((a, b) => {
|
|
679
|
-
const aTime = a.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
680
|
-
const bTime = b.attentionActivityAt?.getTime() ?? Number.NEGATIVE_INFINITY;
|
|
681
|
-
if (aTime !== bTime)
|
|
682
|
-
return bTime - aTime;
|
|
683
|
-
return issueDisplayLabel(a.issue).localeCompare(issueDisplayLabel(b.issue));
|
|
862
|
+
const statusChangeActivity = sourceComment
|
|
863
|
+
? (issueStatusActivityMatchesSourceComment(entry.latestActivity, sourceComment) ? entry.latestActivity : null)
|
|
864
|
+
: entry.latestActivity;
|
|
865
|
+
return issueCard(entry.issue, userId, entry.issue.followed, latestPreview, entry.latestActivityAt, sourceComment, statusChangeActivity);
|
|
684
866
|
});
|
|
685
|
-
const chronologicalItems = options.includeDetail
|
|
686
|
-
? unsortedEntries
|
|
687
|
-
.flatMap((entry) => entry.item ? [entry.item] : [])
|
|
688
|
-
.sort(compareChronologicalActivity)
|
|
689
|
-
: [];
|
|
690
|
-
const latestAttentionEntry = latestFirstEntries.find((entry) => entry.attentionActivityAt);
|
|
691
|
-
const latestActivityAt = latestAttentionEntry?.attentionActivityAt ?? null;
|
|
692
|
-
const unreadCount = unsortedEntries.filter((entry) => {
|
|
693
|
-
const itemActivity = entry.attentionActivityAt;
|
|
694
|
-
if (!itemActivity)
|
|
695
|
-
return false;
|
|
696
|
-
if (!lastReadAt)
|
|
697
|
-
return true;
|
|
698
|
-
return itemActivity.getTime() > lastReadAt.getTime();
|
|
699
|
-
}).length;
|
|
700
867
|
const data = {
|
|
701
|
-
summary: issueSummary(
|
|
702
|
-
itemCount:
|
|
868
|
+
summary: issueSummary(stats.itemCount, stats.latestActivityAt, stats.unreadCount, lastReadAt, latestAttentionEntry?.attentionPreview ?? null),
|
|
869
|
+
itemCount: stats.itemCount,
|
|
703
870
|
};
|
|
704
871
|
if (options.includeDetail) {
|
|
705
872
|
data.detail = {
|
|
706
873
|
threadKey: "issues",
|
|
707
874
|
kind: "issues",
|
|
708
875
|
title: "Issues",
|
|
709
|
-
subtitle: `${
|
|
876
|
+
subtitle: `${stats.itemCount} tracked issue${stats.itemCount === 1 ? "" : "s"}`,
|
|
710
877
|
preview: latestAttentionEntry?.attentionPreview ?? null,
|
|
711
|
-
latestActivityAt,
|
|
878
|
+
latestActivityAt: stats.latestActivityAt,
|
|
712
879
|
lastReadAt,
|
|
713
|
-
unreadCount,
|
|
714
|
-
needsAttention: unreadCount > 0,
|
|
880
|
+
unreadCount: stats.unreadCount,
|
|
881
|
+
needsAttention: stats.unreadCount > 0,
|
|
715
882
|
isPinned: false,
|
|
716
883
|
href: "/messenger/issues",
|
|
717
884
|
description: "Followed issues, issues I created, issues assigned to me, and issues ready for my review",
|
|
718
885
|
items: chronologicalItems,
|
|
886
|
+
pageInfo: {
|
|
887
|
+
limit: detailLimit,
|
|
888
|
+
nextCursor: cursorEntry ? encodeIssueThreadCursor(cursorEntry) : null,
|
|
889
|
+
hasMore: hasMoreDetailEntries,
|
|
890
|
+
},
|
|
719
891
|
};
|
|
720
892
|
}
|
|
721
893
|
return data;
|
|
722
894
|
}
|
|
723
|
-
async function loadIssueSummaryData(orgId, userId, threadStates) {
|
|
724
|
-
const data = await loadIssueData(orgId, userId, threadStates, { includeDetail: true });
|
|
895
|
+
async function loadIssueSummaryData(orgId, userId, threadStates, options = {}) {
|
|
896
|
+
const data = await loadIssueData(orgId, userId, threadStates, { includeDetail: true, ...options });
|
|
725
897
|
return {
|
|
726
898
|
summary: data.summary,
|
|
727
899
|
detail: data.detail,
|
|
@@ -1106,8 +1278,8 @@ export function messengerService(db) {
|
|
|
1106
1278
|
});
|
|
1107
1279
|
return threadSummaries;
|
|
1108
1280
|
}
|
|
1109
|
-
async function getIssuesThread(orgId, userId) {
|
|
1110
|
-
return loadIssueSummaryData(orgId, userId);
|
|
1281
|
+
async function getIssuesThread(orgId, userId, options = {}) {
|
|
1282
|
+
return loadIssueSummaryData(orgId, userId, undefined, options);
|
|
1111
1283
|
}
|
|
1112
1284
|
async function getApprovalsThread(orgId, userId) {
|
|
1113
1285
|
return loadApprovalSummaryData(orgId, userId);
|