feat: unread message tracking with blue dot indicator

Add ContactThreadRead model to track when users last viewed each contact thread.
Contacts with messages newer than the last read time show a blue dot in the sidebar.
Opening a thread automatically marks it as read via markThreadRead mutation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-24 20:25:32 +07:00
parent 643d8d02ba
commit 5492e0d05c
10 changed files with 167 additions and 9 deletions

View File

@@ -87,8 +87,11 @@ export type Contact = {
avatar: Scalars['String']['output'];
channels: Array<Scalars['String']['output']>;
description: Scalars['String']['output'];
hasUnread: Scalars['Boolean']['output'];
id: Scalars['ID']['output'];
lastContactAt: Scalars['String']['output'];
lastMessageChannel: Scalars['String']['output'];
lastMessageText: Scalars['String']['output'];
name: Scalars['String']['output'];
};
@@ -220,6 +223,7 @@ export type Mutation = {
logPilotNote: MutationResult;
login: MutationResult;
logout: MutationResult;
markThreadRead: MutationResult;
rollbackChangeSetItems: MutationResult;
rollbackLatestChangeSet: MutationResult;
selectChatConversation: MutationResult;
@@ -277,6 +281,11 @@ export type MutationloginArgs = {
};
export type MutationmarkThreadReadArgs = {
contactId: Scalars['ID']['input'];
};
export type MutationrollbackChangeSetItemsArgs = {
changeSetId: Scalars['ID']['input'];
itemIds: Array<Scalars['ID']['input']>;
@@ -464,7 +473,7 @@ export type ContactInboxesQueryQuery = { __typename?: 'Query', contactInboxes: A
export type ContactsQueryQueryVariables = Exact<{ [key: string]: never; }>;
export type ContactsQueryQuery = { __typename?: 'Query', contacts: Array<{ __typename?: 'Contact', id: string, name: string, avatar: string, channels: Array<string>, lastContactAt: string, description: string }> };
export type ContactsQueryQuery = { __typename?: 'Query', contacts: Array<{ __typename?: 'Contact', id: string, name: string, avatar: string, channels: Array<string>, lastContactAt: string, lastMessageText: string, lastMessageChannel: string, hasUnread: boolean, description: string }> };
export type CreateCalendarEventMutationMutationVariables = Exact<{
input: CreateCalendarEventInput;
@@ -544,6 +553,13 @@ export type LogoutMutationMutationVariables = Exact<{ [key: string]: never; }>;
export type LogoutMutationMutation = { __typename?: 'Mutation', logout: { __typename?: 'MutationResult', ok: boolean } };
export type MarkThreadReadMutationVariables = Exact<{
contactId: Scalars['ID']['input'];
}>;
export type MarkThreadReadMutation = { __typename?: 'Mutation', markThreadRead: { __typename?: 'MutationResult', ok: boolean } };
export type MeQueryQueryVariables = Exact<{ [key: string]: never; }>;
@@ -921,6 +937,9 @@ export const ContactsQueryDocument = gql`
avatar
channels
lastContactAt
lastMessageText
lastMessageChannel
hasUnread
description
}
}
@@ -1399,6 +1418,35 @@ export function useLogoutMutationMutation(options: VueApolloComposable.UseMutati
return VueApolloComposable.useMutation<LogoutMutationMutation, LogoutMutationMutationVariables>(LogoutMutationDocument, options);
}
export type LogoutMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<LogoutMutationMutation, LogoutMutationMutationVariables>;
export const MarkThreadReadDocument = gql`
mutation MarkThreadRead($contactId: ID!) {
markThreadRead(contactId: $contactId) {
ok
}
}
`;
/**
* __useMarkThreadReadMutation__
*
* To run a mutation, you first call `useMarkThreadReadMutation` within a Vue component and pass it any options that fit your needs.
* When your component renders, `useMarkThreadReadMutation` returns an object that includes:
* - A mutate function that you can call at any time to execute the mutation
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
*
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
*
* @example
* const { mutate, loading, error, onDone } = useMarkThreadReadMutation({
* variables: {
* contactId: // value for 'contactId'
* },
* });
*/
export function useMarkThreadReadMutation(options: VueApolloComposable.UseMutationOptions<MarkThreadReadMutation, MarkThreadReadMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<MarkThreadReadMutation, MarkThreadReadMutationVariables>> = {}) {
return VueApolloComposable.useMutation<MarkThreadReadMutation, MarkThreadReadMutationVariables>(MarkThreadReadDocument, options);
}
export type MarkThreadReadMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<MarkThreadReadMutation, MarkThreadReadMutationVariables>;
export const MeQueryDocument = gql`
query MeQuery {
me {