Skip to content

Commit 0c06e06

Browse files
committed
Fix star logic
1 parent ecf2405 commit 0c06e06

9 files changed

Lines changed: 242 additions & 37 deletions

File tree

apps/api/src/app.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,25 @@ function applyLocalMailboxAction(
793793
}
794794

795795
if (action === "star") {
796+
const message = mailDatabase.getMessage(messageId);
796797
mailDatabase.setMessageStarred(messageId, true);
798+
const starredMailboxId = systemMailboxId(mailDatabase, "starred") ?? "[Gmail]/Starred";
799+
ensureLocalMailbox(mailDatabase, starredMailboxId);
800+
const alreadyInStarred = message?.mailboxIds.includes(starredMailboxId) ?? false;
801+
mailDatabase.saveMailboxEntry({
802+
id: `${messageId}:${starredMailboxId}`,
803+
mailboxId: starredMailboxId,
804+
messageId,
805+
});
806+
if (message?.unread && !alreadyInStarred) {
807+
mailDatabase.adjustMailboxUnreadCount(starredMailboxId, 1);
808+
}
797809
}
798810

799811
if (action === "unstar") {
800812
mailDatabase.setMessageStarred(messageId, false);
813+
const starredMailboxId = systemMailboxId(mailDatabase, "starred") ?? "[Gmail]/Starred";
814+
mailDatabase.removeMailboxEntry(messageId, starredMailboxId);
801815
}
802816

803817
if (action === "archive") {
@@ -825,7 +839,18 @@ function applyLocalMailboxAction(
825839
}
826840
}
827841

828-
function systemMailboxId(mailDatabase: MailDatabase, role: "inbox" | "trash"): string | undefined {
842+
function ensureLocalMailbox(mailDatabase: MailDatabase, mailboxId: string): void {
843+
if (mailDatabase.listMailboxes().some((mailbox) => mailbox.id === mailboxId)) {
844+
return;
845+
}
846+
847+
mailDatabase.saveMailbox({ id: mailboxId, name: mailboxId, unreadCount: 0 });
848+
}
849+
850+
function systemMailboxId(
851+
mailDatabase: MailDatabase,
852+
role: "inbox" | "trash" | "starred",
853+
): string | undefined {
829854
const mailboxes = mailDatabase.listMailboxes();
830855

831856
const roleMailbox = mailboxes.find((mailbox) => mailbox.systemRole === role)?.id;
@@ -839,6 +864,20 @@ function systemMailboxId(mailDatabase: MailDatabase, role: "inbox" | "trash"): s
839864
)?.id;
840865
}
841866

867+
if (role === "starred") {
868+
return (
869+
mailboxes.find(
870+
(mailbox) =>
871+
mailbox.id.toLowerCase().endsWith("/starred") ||
872+
mailbox.name.toLowerCase().endsWith("/starred"),
873+
)?.id ??
874+
mailboxes.find(
875+
(mailbox) =>
876+
mailbox.id.toLowerCase() === "starred" || mailbox.name.toLowerCase() === "starred",
877+
)?.id
878+
);
879+
}
880+
842881
return (
843882
mailboxes.find(
844883
(mailbox) =>

apps/api/src/gmail-imap.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,14 @@ export function createGmailImapMailboxSyncClient(
192192
const id = message.emailId ?? `${mailbox.path}:${message.uid}`;
193193
const stableIdentity = `gmail:${account.id}:${id}`;
194194
const existing = messagesByIdentity.get(stableIdentity);
195+
const starred =
196+
message.flags?.has("\\Flagged") === true || isStarredMailboxId(mailbox.path);
195197

196198
if (existing) {
197199
if (!existing.mailboxIds.includes(mailbox.path)) {
198200
existing.mailboxIds.push(mailbox.path);
199201
}
202+
existing.starred = existing.starred || starred;
200203
continue;
201204
}
202205

@@ -224,6 +227,7 @@ export function createGmailImapMailboxSyncClient(
224227
bccRecipients: message.envelope?.bcc?.map(participantFromAddress),
225228
receivedAt: (receivedAt ?? new Date()).toISOString(),
226229
unread: !message.flags?.has("\\Seen"),
230+
starred,
227231
snippet: body.text.slice(0, 240),
228232
bodyText: body.text,
229233
readableBody: body.html,
@@ -372,6 +376,12 @@ function isNonSelectableMailbox(mailbox: { flags?: Set<string> }): boolean {
372376
return mailbox.flags?.has("\\Noselect") === true || mailbox.flags?.has("\\NonExistent") === true;
373377
}
374378

379+
function isStarredMailboxId(mailboxId: string): boolean {
380+
const normalized = mailboxId.toLowerCase();
381+
382+
return normalized === "starred" || normalized.endsWith("/starred");
383+
}
384+
375385
function participantFromAddress(address: ImapAddress | undefined): {
376386
address: string;
377387
displayName?: string;

apps/api/src/persistence.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ export class MailDatabase {
772772
attachments_json: string;
773773
};
774774
const attachments = JSON.parse(message.attachments_json) as AttachmentMetadata[];
775+
const mailboxIds = this.listMailboxIdsForMessage(message.id);
775776

776777
return {
777778
accountId: resolvedAccountId,
@@ -785,8 +786,8 @@ export class MailDatabase {
785786
bccRecipients: JSON.parse(message.bcc_recipients_json) as MessageParticipant[],
786787
receivedAt: message.received_at,
787788
unread: Boolean(message.unread),
788-
starred: Boolean(message.starred),
789-
mailboxIds: this.listMailboxIdsForMessage(message.id),
789+
starred: starredFromState(Boolean(message.starred), mailboxIds),
790+
mailboxIds,
790791
snippet: message.snippet,
791792
attachmentCount: attachments.length,
792793
updatedAt: message.updated_at || message.received_at,
@@ -851,6 +852,7 @@ export class MailDatabase {
851852
}
852853

853854
const attachments = JSON.parse(row.attachments_json) as AttachmentMetadata[];
855+
const mailboxIds = this.listMailboxIdsForMessage(row.id);
854856

855857
return {
856858
accountId: resolvedAccountId,
@@ -864,8 +866,8 @@ export class MailDatabase {
864866
bccRecipients: JSON.parse(row.bcc_recipients_json) as MessageParticipant[],
865867
receivedAt: row.received_at,
866868
unread: Boolean(row.unread),
867-
starred: Boolean(row.starred),
868-
mailboxIds: this.listMailboxIdsForMessage(row.id),
869+
starred: starredFromState(Boolean(row.starred), mailboxIds),
870+
mailboxIds,
869871
snippet: row.snippet,
870872
attachmentCount: attachments.length,
871873
updatedAt: row.updated_at || row.received_at,
@@ -906,6 +908,7 @@ export class MailDatabase {
906908
attachments_json: string;
907909
};
908910
const attachments = JSON.parse(message.attachments_json) as AttachmentMetadata[];
911+
const mailboxIds = this.listMailboxIdsForMessage(message.id);
909912

910913
return {
911914
accountId: mailAccountId,
@@ -919,8 +922,8 @@ export class MailDatabase {
919922
bccRecipients: JSON.parse(message.bcc_recipients_json) as MessageParticipant[],
920923
receivedAt: message.received_at,
921924
unread: Boolean(message.unread),
922-
starred: Boolean(message.starred),
923-
mailboxIds: this.listMailboxIdsForMessage(message.id),
925+
starred: starredFromState(Boolean(message.starred), mailboxIds),
926+
mailboxIds,
924927
snippet: message.snippet,
925928
attachmentCount: attachments.length,
926929
updatedAt: message.updated_at || message.received_at,
@@ -962,6 +965,7 @@ export class MailDatabase {
962965
attachments_json: string;
963966
};
964967
const attachments = JSON.parse(message.attachments_json) as AttachmentMetadata[];
968+
const mailboxIds = this.listMailboxIdsForMessage(message.id);
965969

966970
return {
967971
accountId: mailAccountId,
@@ -975,8 +979,8 @@ export class MailDatabase {
975979
bccRecipients: JSON.parse(message.bcc_recipients_json) as MessageParticipant[],
976980
receivedAt: message.received_at,
977981
unread: Boolean(message.unread),
978-
starred: Boolean(message.starred),
979-
mailboxIds: this.listMailboxIdsForMessage(message.id),
982+
starred: starredFromState(Boolean(message.starred), mailboxIds),
983+
mailboxIds,
980984
snippet: message.snippet,
981985
attachmentCount: attachments.length,
982986
updatedAt: message.updated_at || message.received_at,
@@ -1024,6 +1028,7 @@ export class MailDatabase {
10241028
}
10251029

10261030
const attachments = JSON.parse(row.attachments_json) as AttachmentMetadata[];
1031+
const mailboxIds = this.listMailboxIdsForMessage(row.id);
10271032

10281033
return {
10291034
accountId: mailAccountId,
@@ -1037,8 +1042,8 @@ export class MailDatabase {
10371042
bccRecipients: JSON.parse(row.bcc_recipients_json) as MessageParticipant[],
10381043
receivedAt: row.received_at,
10391044
unread: Boolean(row.unread),
1040-
starred: Boolean(row.starred),
1041-
mailboxIds: this.listMailboxIdsForMessage(row.id),
1045+
starred: starredFromState(Boolean(row.starred), mailboxIds),
1046+
mailboxIds,
10421047
snippet: row.snippet,
10431048
attachmentCount: attachments.length,
10441049
updatedAt: row.updated_at || row.received_at,
@@ -1135,6 +1140,16 @@ function encodeMessageCursor(receivedAt: string, id: string): string {
11351140
return Buffer.from(JSON.stringify({ receivedAt, id }), "utf8").toString("base64url");
11361141
}
11371142

1143+
function starredFromState(starred: boolean, mailboxIds: string[]): boolean {
1144+
return starred || mailboxIds.some((mailboxId) => isStarredLikeMailboxId(mailboxId));
1145+
}
1146+
1147+
function isStarredLikeMailboxId(mailboxId: string): boolean {
1148+
const normalized = mailboxId.toLowerCase();
1149+
1150+
return normalized === "starred" || normalized.endsWith("/starred");
1151+
}
1152+
11381153
function isTrashLikeMailboxId(mailboxId: string): boolean {
11391154
const normalized = mailboxId.toLowerCase();
11401155

apps/api/src/sync.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type ImapMessage = {
4242
}>;
4343
receivedAt: string;
4444
unread: boolean;
45+
starred: boolean;
4546
snippet?: string;
4647
bodyText?: string;
4748
readableBody: string;
@@ -282,7 +283,7 @@ export async function syncRecentMessages({
282283
bccRecipients: message.bccRecipients,
283284
receivedAt: message.receivedAt,
284285
unread: message.unread,
285-
starred: false,
286+
starred: message.starred,
286287
aiProcessed: false,
287288
snippet: message.snippet,
288289
bodyText: message.bodyText,
@@ -393,7 +394,7 @@ export async function syncRecentReconciliation({
393394
bccRecipients: message.bccRecipients,
394395
receivedAt: message.receivedAt,
395396
unread: message.unread,
396-
starred: false,
397+
starred: message.starred,
397398
aiProcessed: false,
398399
snippet: message.snippet,
399400
bodyText: message.bodyText,

apps/web/src/App.vue

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type {
33
MailAccountMailboxTree,
44
MailboxAction,
5+
MailboxMessagesResponse,
56
MailboxMessageSummary,
67
MailboxSummary,
78
MessageDetail,
@@ -270,7 +271,24 @@ const syncJobMutation = useMutation({
270271
const mailboxActionMutation = useMutation({
271272
mutationFn: ({ messageId, action }: { messageId: string; action: MailboxAction }) =>
272273
performMailboxAction(selectedAccountId.value, messageId, action),
273-
onSuccess: async (_, variables) => {
274+
onSuccess: async (response, variables) => {
275+
queryClient.setQueryData(
276+
["message-detail", response.message.accountId, response.message.id],
277+
response,
278+
);
279+
queryClient.setQueriesData<MailboxMessagesResponse>({ queryKey: ["message-list"] }, (page) =>
280+
page
281+
? {
282+
...page,
283+
messages: page.messages.map((message) =>
284+
message.accountId === response.message.accountId && message.id === response.message.id
285+
? { ...message, ...response.message }
286+
: message,
287+
),
288+
}
289+
: page,
290+
);
291+
274292
if (variables.action === "archive" || variables.action === "delete") {
275293
openAdjacentMessage(variables.messageId);
276294
}
@@ -450,6 +468,12 @@ function syncJobsPollingInterval(query: { state: { data: SyncJobsResponse | unde
450468
451469
function accountContextMenuItems(account: MailAccountMailboxTree) {
452470
return [
471+
{
472+
label: "Sync now",
473+
icon: "i-lucide-refresh-cw",
474+
disabled: syncingAccountIds.value.has(account.id) || syncJobMutation.isPending.value,
475+
onSelect: () => syncJobMutation.mutate({ accountId: account.id }),
476+
},
453477
{
454478
label: "Custom sync...",
455479
icon: "i-lucide-calendar-clock",
@@ -522,6 +546,10 @@ function syncJobScopeLabel(job: SyncJobRecord): string {
522546
return `Custom ${job.scope.days}d`;
523547
}
524548
549+
function syncJobOriginLabel(job: SyncJobRecord): string {
550+
return job.origin === "automatic" ? "" : "👤";
551+
}
552+
525553
function syncJobTime(job: SyncJobRecord): string {
526554
return formatDate(job.finishedAt ?? job.startedAt ?? job.createdAt);
527555
}
@@ -873,12 +901,14 @@ async function selectAccountDefault(account: MailAccountMailboxTree) {
873901
</span>
874902
</div>
875903
<div class="mt-1 flex items-center justify-between gap-2 text-slate-500">
876-
<span class="truncate">{{ syncJobScopeLabel(job) }} · {{ job.state }}</span>
904+
<span class="truncate"
905+
>{{ syncJobOriginLabel(job) }} {{ syncJobScopeLabel(job) }} ·
906+
{{ syncJobSummary(job) }}</span
907+
>
877908
<span v-if="syncJobDuration(job)" class="shrink-0">{{
878909
syncJobDuration(job)
879910
}}</span>
880911
</div>
881-
<div class="mt-1 truncate text-slate-600">{{ syncJobSummary(job) }}</div>
882912
</div>
883913
</div>
884914
</div>
@@ -981,18 +1011,6 @@ async function selectAccountDefault(account: MailAccountMailboxTree) {
9811011
variant="subtle"
9821012
>{{ account.unreadCount }}</UBadge
9831013
>
984-
<UButton
985-
color="neutral"
986-
icon="i-lucide-refresh-cw"
987-
:loading="syncingAccountIds.has(account.id)"
988-
:disabled="
989-
syncingAccountIds.has(account.id) || syncJobMutation.isPending.value
990-
"
991-
square
992-
variant="ghost"
993-
aria-label="Refresh account"
994-
@click="syncJobMutation.mutate({ accountId: account.id })"
995-
/>
9961014
</div>
9971015
</div>
9981016
</UContextMenu>
@@ -1134,18 +1152,22 @@ async function selectAccountDefault(account: MailAccountMailboxTree) {
11341152
message.unread
11351153
? 'border-l-4 border-l-slate-800 bg-stone-50'
11361154
: 'border-l-4 border-l-transparent bg-stone-100',
1155+
message.starred ? 'bg-amber-50' : '',
11371156
selectedMessageId === message.id ? 'bg-stone-200' : '',
11381157
]"
11391158
type="button"
11401159
@click="selectMessage(message.id)"
11411160
>
11421161
<div class="flex items-center justify-between gap-2">
1143-
<span
1144-
class="truncate text-xs"
1145-
:class="message.unread ? 'font-bold text-slate-950' : 'font-medium'"
1146-
>
1147-
{{ senderLabel(message) }}
1148-
</span>
1162+
<div class="flex min-w-0 items-center gap-1">
1163+
<span v-if="message.starred" class="shrink-0 text-amber-500">★</span>
1164+
<span
1165+
class="truncate text-xs"
1166+
:class="message.unread ? 'font-bold text-slate-950' : 'font-medium'"
1167+
>
1168+
{{ senderLabel(message) }}
1169+
</span>
1170+
</div>
11491171
<span class="shrink-0 text-[11px] text-slate-500">{{
11501172
formatDate(message.receivedAt)
11511173
}}</span>

0 commit comments

Comments
 (0)