@droppii-org/chat-mobile 0.2.105 → 0.2.106
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/lib/module/components/TypingDots/TypingDots.js +131 -0
- package/lib/module/components/TypingDots/TypingDots.js.map +1 -0
- package/lib/module/components/TypingDots/index.js +4 -0
- package/lib/module/components/TypingDots/index.js.map +1 -0
- package/lib/module/core/useChatListener.js +26 -2
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/groups/useGroupMemberName.js +32 -0
- package/lib/module/hooks/groups/useGroupMemberName.js.map +1 -0
- package/lib/module/hooks/query-keys.js +2 -1
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/useInputState.js +38 -0
- package/lib/module/hooks/useInputState.js.map +1 -0
- package/lib/module/hooks/useInputStateBroadcast.js +53 -0
- package/lib/module/hooks/useInputStateBroadcast.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatComposer.js +15 -3
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js +2 -1
- package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatTypingIndicator.js +86 -0
- package/lib/module/screens/chat-detail/ChatTypingIndicator.js.map +1 -0
- package/lib/module/store/index.js +1 -0
- package/lib/module/store/index.js.map +1 -1
- package/lib/module/store/inputState.js +64 -0
- package/lib/module/store/inputState.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +2 -0
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/inputState.js +77 -0
- package/lib/module/utils/inputState.js.map +1 -0
- package/lib/module/utils/message.js +9 -0
- package/lib/module/utils/message.js.map +1 -1
- package/lib/typescript/src/components/TypingDots/TypingDots.d.ts +14 -0
- package/lib/typescript/src/components/TypingDots/TypingDots.d.ts.map +1 -0
- package/lib/typescript/src/components/TypingDots/index.d.ts +3 -0
- package/lib/typescript/src/components/TypingDots/index.d.ts.map +1 -0
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useGroupMemberName.d.ts +10 -0
- package/lib/typescript/src/hooks/groups/useGroupMemberName.d.ts.map +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useInputState.d.ts +21 -0
- package/lib/typescript/src/hooks/useInputState.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useInputStateBroadcast.d.ts +14 -0
- package/lib/typescript/src/hooks/useInputStateBroadcast.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatTypingIndicator.d.ts +8 -0
- package/lib/typescript/src/screens/chat-detail/ChatTypingIndicator.d.ts.map +1 -0
- package/lib/typescript/src/store/index.d.ts +1 -0
- package/lib/typescript/src/store/index.d.ts.map +1 -1
- package/lib/typescript/src/store/inputState.d.ts +18 -0
- package/lib/typescript/src/store/inputState.d.ts.map +1 -0
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/inputState.d.ts +54 -0
- package/lib/typescript/src/utils/inputState.d.ts.map +1 -0
- package/lib/typescript/src/utils/message.d.ts +8 -0
- package/lib/typescript/src/utils/message.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/TypingDots/TypingDots.tsx +172 -0
- package/src/components/TypingDots/index.ts +2 -0
- package/src/core/useChatListener.ts +35 -1
- package/src/hooks/groups/useGroupMemberName.ts +31 -0
- package/src/hooks/query-keys.ts +2 -0
- package/src/hooks/useInputState.ts +57 -0
- package/src/hooks/useInputStateBroadcast.ts +53 -0
- package/src/screens/chat-detail/ChatComposer.tsx +17 -2
- package/src/screens/chat-detail/ChatListLegend.tsx +2 -0
- package/src/screens/chat-detail/ChatTypingIndicator.tsx +89 -0
- package/src/store/index.ts +1 -0
- package/src/store/inputState.ts +76 -0
- package/src/translation/resources/i18n.ts +2 -0
- package/src/utils/inputState.ts +104 -0
- package/src/utils/message.ts +15 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["PeerType","SessionType","getConversationID","getGroupIdFromConversationId","getMessageTime","message","sendTime","createTime","getMessageKey","clientMsgID","serverMsgID","getMessageRenderId","index","mergeMessages","current","incoming","map","Map","key","set","Array","from","values","sort","a","b","normalizeCreatedMessage","normalized","JSON","parse","Error","resolveHistoryLastMinSeq","history","fallbackMessage","lastMinSeq","seq","getHistoryPaginationAnchor","messages","length","oldest","reduce","prev","curr","hasNewHistoryMessages","currentKeys","Set","filter","Boolean","some","has","
|
|
1
|
+
{"version":3,"names":["PeerType","SessionType","getConversationID","getGroupIdFromConversationId","getMessageTime","message","sendTime","createTime","getMessageKey","clientMsgID","serverMsgID","getMessageRenderId","index","mergeMessages","current","incoming","map","Map","key","set","Array","from","values","sort","a","b","normalizeCreatedMessage","normalized","JSON","parse","Error","resolveHistoryLastMinSeq","history","fallbackMessage","lastMinSeq","seq","getHistoryPaginationAnchor","messages","length","oldest","reduce","prev","curr","hasNewHistoryMessages","currentKeys","Set","filter","Boolean","some","has","conversationIdsOfMessage","sessionType","Group","groupID","Single","sendID","recvID","belongsToConversation","conversationId","targetUserId","forwardId","reverseId","getReceiverId","conversation","peerType","conversationID","userID"],"sourceRoot":"../../../src","sources":["utils/message.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SvgProps } from 'react-native-svg';
|
|
2
|
+
export interface TypingDotsProps extends Omit<SvgProps, 'fill'> {
|
|
3
|
+
/**
|
|
4
|
+
* Dot colour. Overrides the `fill` SvgProps would otherwise carry, because
|
|
5
|
+
* that one lands on the root <Svg> where the dots' own group already shadows
|
|
6
|
+
* it — passing it there does nothing.
|
|
7
|
+
*
|
|
8
|
+
* Only the colour: the source animates `fill: rgba(26, 29, 34, a)` but never
|
|
9
|
+
* changes the RGB, so the alpha half is driven separately as `fillOpacity`.
|
|
10
|
+
*/
|
|
11
|
+
fill?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const TypingDots: import("react").MemoExoticComponent<({ fill, width, height, ...props }: TypingDotsProps) => import("react").JSX.Element>;
|
|
14
|
+
//# sourceMappingURL=TypingDots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TypingDots.d.ts","sourceRoot":"","sources":["../../../../../src/components/TypingDots/TypingDots.tsx"],"names":[],"mappings":"AAYA,OAAY,EAAW,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAwD/D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC7D;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAkED,eAAO,MAAM,UAAU,0EAMlB,eAAe,iCAmBnB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/TypingDots/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useChatListener.d.ts","sourceRoot":"","sources":["../../../../src/core/useChatListener.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useChatListener.d.ts","sourceRoot":"","sources":["../../../../src/core/useChatListener.ts"],"names":[],"mappings":"AAuCA,eAAO,MAAM,eAAe,YAid3B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display name of one group member.
|
|
3
|
+
*
|
|
4
|
+
* Fetches a single member rather than the whole roster: the only place this is
|
|
5
|
+
* needed is the typing sentence, which names at most one person — beyond that
|
|
6
|
+
* the copy collapses to "Nhiều người". Cached by react-query so a dot flickering
|
|
7
|
+
* on and off does not re-hit the SDK.
|
|
8
|
+
*/
|
|
9
|
+
export declare const useGroupMemberName: (groupID?: string, userID?: string) => string | undefined;
|
|
10
|
+
//# sourceMappingURL=useGroupMemberName.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGroupMemberName.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/groups/useGroupMemberName.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,EAChB,SAAS,MAAM,KACd,MAAM,GAAG,SAeX,CAAC"}
|
|
@@ -55,5 +55,6 @@ export declare const groupQueryKeys: {
|
|
|
55
55
|
join: () => readonly ["groups", "join"];
|
|
56
56
|
leave: () => readonly ["groups", "leave"];
|
|
57
57
|
inviteInfo: (code: string) => readonly ["groups", "inviteInfo", string];
|
|
58
|
+
member: (groupID?: string, userID?: string) => readonly ["groups", "member", string, string];
|
|
58
59
|
};
|
|
59
60
|
//# sourceMappingURL=query-keys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-keys.d.ts","sourceRoot":"","sources":["../../../../src/hooks/query-keys.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe;;uBAEP,MAAM;CAC1B,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;4BAGR,MAAM;;6BAGL,MAAM;0BAET,MAAM;yCACS,MAAM;2BAEpB,MAAM;mCACE,MAAM;gCAIT,MAAM;8BAIR,MAAM;mCACD,MAAM;2BAId,MAAM;iCACA,MAAM;4BACX,MAAM;sBACZ,MAAM;yBACH,MAAM;CAC5B,CAAC;AAEF,eAAO,MAAM,cAAc;;oBAET,MAAM,EAAE;CACzB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC;AAEjE,eAAO,MAAM,gBAAgB;;;6BAGF,MAAM;;CAGhC,CAAC;AAEF,eAAO,MAAM,uBAAuB;;qBAEjB,MAAM,eAAe,MAAM;CAE7C,CAAC;AAEF,eAAO,MAAM,gBAAgB;;oBAEX,MAAM,mBAAmB,MAAM;CAEhD,CAAC;AAEF,eAAO,MAAM,gBAAgB;;sBAET,MAAM,WAAW,MAAM;CAE1C,CAAC;AAEF,eAAO,MAAM,gBAAgB;;oBAEX,MAAM,SAAS,MAAM;CAEtC,CAAC;AAEF,eAAO,MAAM,cAAc;;;4BAGD,MAAM;;;uBAIX,MAAM;
|
|
1
|
+
{"version":3,"file":"query-keys.d.ts","sourceRoot":"","sources":["../../../../src/hooks/query-keys.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe;;uBAEP,MAAM;CAC1B,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;4BAGR,MAAM;;6BAGL,MAAM;0BAET,MAAM;yCACS,MAAM;2BAEpB,MAAM;mCACE,MAAM;gCAIT,MAAM;8BAIR,MAAM;mCACD,MAAM;2BAId,MAAM;iCACA,MAAM;4BACX,MAAM;sBACZ,MAAM;yBACH,MAAM;CAC5B,CAAC;AAEF,eAAO,MAAM,cAAc;;oBAET,MAAM,EAAE;CACzB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,OAAO,qBAAqB,CAAC;AAEjE,eAAO,MAAM,gBAAgB;;;6BAGF,MAAM;;CAGhC,CAAC;AAEF,eAAO,MAAM,uBAAuB;;qBAEjB,MAAM,eAAe,MAAM;CAE7C,CAAC;AAEF,eAAO,MAAM,gBAAgB;;oBAEX,MAAM,mBAAmB,MAAM;CAEhD,CAAC;AAEF,eAAO,MAAM,gBAAgB;;sBAET,MAAM,WAAW,MAAM;CAE1C,CAAC;AAEF,eAAO,MAAM,gBAAgB;;oBAEX,MAAM,SAAS,MAAM;CAEtC,CAAC;AAEF,eAAO,MAAM,cAAc;;;4BAGD,MAAM;;;uBAIX,MAAM;uBAEN,MAAM,WAAW,MAAM;CAE3C,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type InputStateRecord } from '../utils/inputState';
|
|
2
|
+
export interface InputStateResult {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the indicator belongs on screen. Unlike the sentence this needs no
|
|
5
|
+
* name, so it can be answered here.
|
|
6
|
+
*/
|
|
7
|
+
isTyping: boolean;
|
|
8
|
+
/** Everyone still inside the window, oldest signal first. */
|
|
9
|
+
typers: readonly InputStateRecord[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Typing state for one conversation.
|
|
13
|
+
*
|
|
14
|
+
* Expiry is read from each record's timestamp rather than tracked with a timer
|
|
15
|
+
* per user, and the hook schedules a single wake-up at the moment the next
|
|
16
|
+
* typer falls out of the window. So with A typing at t=0 and B at t=2s the
|
|
17
|
+
* sentence moves from A, to both, to B alone at t=3s, and disappears at t=5s —
|
|
18
|
+
* without polling between those points.
|
|
19
|
+
*/
|
|
20
|
+
export declare const useInputState: (conversationID?: string) => InputStateResult;
|
|
21
|
+
//# sourceMappingURL=useInputState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInputState.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useInputState.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,6DAA6D;IAC7D,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACrC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,iBAAiB,MAAM,KAAG,gBA6BvD,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcasts this device's composing state for a conversation.
|
|
3
|
+
*
|
|
4
|
+
* The SDK throttles the outgoing frames itself, so `notifyTyping` is safe to
|
|
5
|
+
* call on every keystroke — no debounce here would only duplicate that. What
|
|
6
|
+
* this hook does own is the start/stop rule: an empty draft counts as stopping,
|
|
7
|
+
* not as typing, and leaving the screen has to stop too or the peer is left
|
|
8
|
+
* watching an indicator for a composer nobody is in front of.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useInputStateBroadcast: (conversationID?: string) => {
|
|
11
|
+
notifyTyping: (text?: string) => void;
|
|
12
|
+
notifyStoppedTyping: () => void;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=useInputStateBroadcast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInputStateBroadcast.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useInputStateBroadcast.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,iBAAiB,MAAM;0BAyBlD,MAAM;;CAejB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatComposer.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,IAAI,EACV,MAAM,cAAc,CAAC;AA8BtB,OAAO,EAA6B,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatComposer.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatComposer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAIL,KAAK,iBAAiB,EACtB,KAAK,IAAI,EACV,MAAM,cAAc,CAAC;AA8BtB,OAAO,EAA6B,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAgC5E,UAAU,yBAAyB;IACjC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACrC,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACtD,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,YAAY,+JASpB,yBAAyB,iCA6oB7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatListLegend.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatListLegend.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAQf,OAAiB,EAKf,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AActC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatListLegend.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatListLegend.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAQf,OAAiB,EAKf,KAAK,WAAW,EACjB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AActC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAoF7C,UAAU,2BAA4B,SAAQ,aAAa;IACzD,OAAO,EAAE,SAAS,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACzC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,kBAAkB,EAAE,CAAC,IAAI,EAAE;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,aAAa,EAAE,OAAO,CAAC;KACxB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,2SAmBtB,2BAA2B,iCAq2B/B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "<name> đang nhập..." above the composer, with the bouncing dots.
|
|
3
|
+
*
|
|
4
|
+
* Renders nothing while nobody is typing — an empty row would reserve height
|
|
5
|
+
* and nudge the message list every time somebody started or stopped.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ChatTypingIndicator: import("react").MemoExoticComponent<() => import("react").JSX.Element>;
|
|
8
|
+
//# sourceMappingURL=ChatTypingIndicator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatTypingIndicator.d.ts","sourceRoot":"","sources":["../../../../../src/screens/chat-detail/ChatTypingIndicator.tsx"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,wEAsD9B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/store/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/store/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConversationInputStatus } from '@droppii/openim-rn-client-sdk';
|
|
2
|
+
import type { InputStateMap } from '../utils/inputState';
|
|
3
|
+
export interface InputStateStore {
|
|
4
|
+
/** conversationID -> userID -> record. */
|
|
5
|
+
byConversation: InputStateMap;
|
|
6
|
+
/**
|
|
7
|
+
* Apply one OnInputStatusChanged payload.
|
|
8
|
+
*
|
|
9
|
+
* A non-empty `platformIDs` means that user is composing; an empty list is
|
|
10
|
+
* how the SDK reports that they stopped, so it removes them outright rather
|
|
11
|
+
* than storing an inactive record.
|
|
12
|
+
*/
|
|
13
|
+
applyInputStatus: (status: ConversationInputStatus) => void;
|
|
14
|
+
/** Remove one user now — their message arrived, or they went idle. */
|
|
15
|
+
removeTyper: (conversationID: string, userID: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const useInputStateStore: import("zustand").UseBoundStore<import("zustand").StoreApi<InputStateStore>>;
|
|
18
|
+
//# sourceMappingURL=inputState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputState.d.ts","sourceRoot":"","sources":["../../../../src/store/inputState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAEV,aAAa,EACd,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,0CAA0C;IAC1C,cAAc,EAAE,aAAa,CAAC;IAC9B;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAC5D,sEAAsE;IACtE,WAAW,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/D;AAaD,eAAO,MAAM,kBAAkB,8EA0C5B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../../../../src/translation/resources/i18n.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../../../../src/translation/resources/i18n.ts"],"names":[],"mappings":";;;AA+OA,wBAEE"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Platform } from '@droppii/openim-rn-client-sdk';
|
|
2
|
+
/** A typer drops out this long after their last signal — BR-002. */
|
|
3
|
+
export declare const INPUT_STATE_TTL = 3000;
|
|
4
|
+
export interface InputStateRecord {
|
|
5
|
+
userID: string;
|
|
6
|
+
/** Devices that user is composing from. An empty list means they stopped. */
|
|
7
|
+
platformIDs: Platform[];
|
|
8
|
+
/** When their most recent signal arrived — the clock the TTL runs against. */
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
}
|
|
11
|
+
/** userID -> record, for one conversation. */
|
|
12
|
+
export type ConversationInputState = Readonly<Record<string, InputStateRecord>>;
|
|
13
|
+
/** conversationID -> that conversation's typers. */
|
|
14
|
+
export type InputStateMap = Readonly<Record<string, ConversationInputState>>;
|
|
15
|
+
/**
|
|
16
|
+
* Typers still inside the TTL window, oldest signal first.
|
|
17
|
+
*
|
|
18
|
+
* Expiry is derived from `updatedAt` rather than driven by one timer per user:
|
|
19
|
+
* the whole point is that each typer runs on their own clock. With A typing at
|
|
20
|
+
* t=0 and B at t=2s, A has to drop at t=3s while B stays until t=5s — a single
|
|
21
|
+
* shared timer reset by the newest signal would keep both alive to t=5s.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getActiveTypers: (state: ConversationInputState | undefined, now: number) => readonly InputStateRecord[];
|
|
24
|
+
/**
|
|
25
|
+
* When the next active typer falls out of the window, or null if none will.
|
|
26
|
+
*
|
|
27
|
+
* The caller uses this to wake up exactly once per expiry instead of polling.
|
|
28
|
+
*/
|
|
29
|
+
export declare const getNextExpiryAt: (state: ConversationInputState | undefined, now: number) => number | null;
|
|
30
|
+
export declare const truncateTypingName: (name: string) => string;
|
|
31
|
+
/**
|
|
32
|
+
* Which copy to render, as a key plus its values.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not a finished string: the name is styled differently from the
|
|
35
|
+
* rest of the sentence, so the caller needs the pieces separately to hand them
|
|
36
|
+
* to a rich-text renderer. Keeping translation out of here also leaves this
|
|
37
|
+
* module free of any dependency on the i18n layer.
|
|
38
|
+
*/
|
|
39
|
+
export interface TypingTextDescriptor {
|
|
40
|
+
key: string;
|
|
41
|
+
values?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
export declare const TYPING_TEXT_KEYS: {
|
|
44
|
+
readonly single: "chat:typing_single";
|
|
45
|
+
readonly multiple: "chat:typing_multiple";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Copy for a set of typers, or null when the indicator should be hidden.
|
|
49
|
+
*
|
|
50
|
+
* Only one typer is ever named. Beyond that the sentence collapses, so the
|
|
51
|
+
* caller never has to resolve a second name.
|
|
52
|
+
*/
|
|
53
|
+
export declare const resolveTypingText: (typerCount: number, singleName?: string) => TypingTextDescriptor | null;
|
|
54
|
+
//# sourceMappingURL=inputState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputState.d.ts","sourceRoot":"","sources":["../../../../src/utils/inputState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAE9D,oEAAoE;AACpE,eAAO,MAAM,eAAe,OAAQ,CAAC;AAKrC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,WAAW,EAAE,QAAQ,EAAE,CAAC;IACxB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8CAA8C;AAC9C,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAEhF,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAC;AAI7E;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,sBAAsB,GAAG,SAAS,EACzC,KAAK,MAAM,KACV,SAAS,gBAAgB,EAU3B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,sBAAsB,GAAG,SAAS,EACzC,KAAK,MAAM,KACV,MAAM,GAAG,IAMX,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MAGxC,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,eAAO,MAAM,gBAAgB;;;CAGnB,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,YAAY,MAAM,EAClB,aAAa,MAAM,KAClB,oBAAoB,GAAG,IAQzB,CAAC"}
|
|
@@ -27,6 +27,14 @@ export declare const resolveHistoryLastMinSeq: (history: {
|
|
|
27
27
|
}, fallbackMessage?: MessageItem) => number;
|
|
28
28
|
export declare const getHistoryPaginationAnchor: (messages: MessageItem[]) => HistoryPaginationAnchor | null;
|
|
29
29
|
export declare const hasNewHistoryMessages: (current: MessageItem[], incoming: MessageItem[]) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Conversation ids a message could be filed under.
|
|
32
|
+
*
|
|
33
|
+
* A 1:1 id is built from both participants and their order is not recoverable
|
|
34
|
+
* from the message alone, so both spellings come back — `belongsToConversation`
|
|
35
|
+
* compares against both for exactly the same reason.
|
|
36
|
+
*/
|
|
37
|
+
export declare const conversationIdsOfMessage: (message: MessageItem) => string[];
|
|
30
38
|
export declare const belongsToConversation: (message: MessageItem, conversationId?: string, targetUserId?: string) => boolean;
|
|
31
39
|
export declare const getReceiverId: (conversation?: ConversationItem) => {
|
|
32
40
|
groupID: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/utils/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAMvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,WACN,CAAC;AAK9C;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,WACA,CAAC;AAEvE,eAAO,MAAM,aAAa,GACxB,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,WAAW,EAab,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,WAAW,GAAG,MAAM,KAC5B,WAYF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAChC,kBAAkB,WAAW,KAC5B,MAMF,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,UAAU,WAAW,EAAE,KACtB,uBAAuB,GAAG,IAkB5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,OASF,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EACpB,iBAAiB,MAAM,EACvB,eAAe,MAAM,KACpB,OA2BF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,eAAe,gBAAgB;;;CAW5D,CAAC"}
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../../src/utils/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,+BAA+B,CAAC;AAMvC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,WACN,CAAC;AAK9C;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,WAAW,EAAE,OAAO,MAAM,WACA,CAAC;AAEvE,eAAO,MAAM,aAAa,GACxB,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,WAAW,EAab,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,SAAS,WAAW,GAAG,MAAM,KAC5B,WAYF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EAChC,kBAAkB,WAAW,KAC5B,MAMF,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,UAAU,WAAW,EAAE,KACtB,uBAAuB,GAAG,IAkB5B,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EAAE,EACtB,UAAU,WAAW,EAAE,KACtB,OASF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GAAI,SAAS,WAAW,KAAG,MAAM,EAM/D,CAAC;AAER,eAAO,MAAM,qBAAqB,GAChC,SAAS,WAAW,EACpB,iBAAiB,MAAM,EACvB,eAAe,MAAM,KACpB,OA2BF,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,eAAe,gBAAgB;;;CAW5D,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { memo, useEffect } from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
Easing,
|
|
4
|
+
useAnimatedProps,
|
|
5
|
+
useReducedMotion,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withDelay,
|
|
8
|
+
withRepeat,
|
|
9
|
+
withSequence,
|
|
10
|
+
withSpring,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import Svg, { G, Rect, type SvgProps } from 'react-native-svg';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The three bouncing dots from `assets/images/typing_indicator.svg`.
|
|
17
|
+
*
|
|
18
|
+
* SVGR cannot carry this one over: the motion lives in a `<style>` block of
|
|
19
|
+
* `@keyframes`, which it drops ("elements not supported by react-native-svg"),
|
|
20
|
+
* leaving `style={{ animation: ... }}` props pointing at rules that no longer
|
|
21
|
+
* exist. react-native-svg has no CSS animation engine either, so the keyframes
|
|
22
|
+
* are re-expressed here as reanimated timelines instead.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const VIEW_WIDTH = 48;
|
|
26
|
+
const VIEW_HEIGHT = 20;
|
|
27
|
+
const DOT_SIZE = 8;
|
|
28
|
+
/** x of each dot in the source — `translate(6 6)`, `(20 6)`, `(34 6)`. */
|
|
29
|
+
const DOT_X = [6, 20, 34];
|
|
30
|
+
const DOT_Y = 6;
|
|
31
|
+
/** The `translateY(-6px)` keyframe. */
|
|
32
|
+
const LIFT = 6;
|
|
33
|
+
|
|
34
|
+
const CYCLE_MS = 3150;
|
|
35
|
+
/** 7.14% of the cycle — the gap between one dot bouncing and the next. */
|
|
36
|
+
const BOUNCE_MS = CYCLE_MS / 14;
|
|
37
|
+
/** 14.29% of the cycle — one step of the opacity wave. */
|
|
38
|
+
const FADE_STEP_MS = CYCLE_MS / 7;
|
|
39
|
+
const FADE_STEPS = 7;
|
|
40
|
+
|
|
41
|
+
const HOLD_SHORT_MS = 900; // 14.29% → 42.86%
|
|
42
|
+
const HOLD_LONG_MS = 1350; // 57.14% → 100%
|
|
43
|
+
|
|
44
|
+
/** The three alpha levels the wave cycles through. */
|
|
45
|
+
const ALPHAS = [0.2, 0.35, 0.5];
|
|
46
|
+
|
|
47
|
+
/** The source's own colour, used when the caller does not pass one. */
|
|
48
|
+
const DEFAULT_FILL = '#1A1D22';
|
|
49
|
+
|
|
50
|
+
const RISE = {
|
|
51
|
+
duration: BOUNCE_MS,
|
|
52
|
+
easing: Easing.out(Easing.ease),
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The fall, not the rise, carries the character here: the source's `linear()`
|
|
57
|
+
* curve peaks at 1.364, so a dot dips 36.4% past its resting line before
|
|
58
|
+
* settling. A spring's first overshoot is exp(-piZ / sqrt(1 - Z^2)), which
|
|
59
|
+
* lands on 36.4% at a damping ratio of ~0.306 — 0.5 would reach only 16%,
|
|
60
|
+
* visibly flatter than drawn.
|
|
61
|
+
*
|
|
62
|
+
* `duration` matters as much as the ratio: a default spring settles whenever it
|
|
63
|
+
* settles, which would break the fixed-length cycle the two tracks share.
|
|
64
|
+
*/
|
|
65
|
+
const FALL = { duration: BOUNCE_MS, dampingRatio: 0.306 } as const;
|
|
66
|
+
|
|
67
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
68
|
+
|
|
69
|
+
export interface TypingDotsProps extends Omit<SvgProps, 'fill'> {
|
|
70
|
+
/**
|
|
71
|
+
* Dot colour. Overrides the `fill` SvgProps would otherwise carry, because
|
|
72
|
+
* that one lands on the root <Svg> where the dots' own group already shadows
|
|
73
|
+
* it — passing it there does nothing.
|
|
74
|
+
*
|
|
75
|
+
* Only the colour: the source animates `fill: rgba(26, 29, 34, a)` but never
|
|
76
|
+
* changes the RGB, so the alpha half is driven separately as `fillOpacity`.
|
|
77
|
+
*/
|
|
78
|
+
fill?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const Dot = memo(({ index }: { index: number }) => {
|
|
82
|
+
const reducedMotion = useReducedMotion();
|
|
83
|
+
const lift = useSharedValue(0);
|
|
84
|
+
const alpha = useSharedValue(ALPHAS[index % ALPHAS.length]!);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (reducedMotion) return;
|
|
88
|
+
|
|
89
|
+
// Two bounces per cycle. A dot's turn comes one BOUNCE_MS after the one
|
|
90
|
+
// before it, and that delay applies only to the first pass — every
|
|
91
|
+
// iteration runs exactly CYCLE_MS, so the offset holds instead of drifting.
|
|
92
|
+
lift.value = withDelay(
|
|
93
|
+
index * BOUNCE_MS,
|
|
94
|
+
withRepeat(
|
|
95
|
+
withSequence(
|
|
96
|
+
withTiming(-LIFT, RISE),
|
|
97
|
+
withSpring(0, FALL),
|
|
98
|
+
withTiming(0, { duration: HOLD_SHORT_MS }),
|
|
99
|
+
withTiming(-LIFT, RISE),
|
|
100
|
+
withSpring(0, FALL),
|
|
101
|
+
withTiming(0, { duration: HOLD_LONG_MS })
|
|
102
|
+
),
|
|
103
|
+
-1
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// The opacity wave needs no delay: each dot enters the cycle at a different
|
|
108
|
+
// alpha instead, which is how the source staggers it.
|
|
109
|
+
alpha.value = withRepeat(
|
|
110
|
+
withSequence(
|
|
111
|
+
...Array.from({ length: FADE_STEPS }, (_, step) =>
|
|
112
|
+
withTiming(ALPHAS[(index + step + 1) % ALPHAS.length]!, {
|
|
113
|
+
duration: FADE_STEP_MS,
|
|
114
|
+
easing: Easing.inOut(Easing.ease),
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
-1
|
|
119
|
+
);
|
|
120
|
+
}, [index, lift, alpha, reducedMotion]);
|
|
121
|
+
|
|
122
|
+
// Driven as SVG attributes rather than a style transform: react-native-svg
|
|
123
|
+
// maps these straight onto the native node, keeping the work off the JS
|
|
124
|
+
// thread. `y` also replaces the source's stacked translateY pair.
|
|
125
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
126
|
+
y: DOT_Y + lift.value,
|
|
127
|
+
fillOpacity: alpha.value,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<AnimatedRect
|
|
132
|
+
x={DOT_X[index]}
|
|
133
|
+
width={DOT_SIZE}
|
|
134
|
+
height={DOT_SIZE}
|
|
135
|
+
// The file says rx="50"; SVG clamps that to half the width. Stating the
|
|
136
|
+
// clamped value keeps the circle from depending on that behaviour.
|
|
137
|
+
rx={DOT_SIZE / 2}
|
|
138
|
+
animatedProps={animatedProps}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
Dot.displayName = 'TypingDots.Dot';
|
|
144
|
+
|
|
145
|
+
export const TypingDots = memo(
|
|
146
|
+
({
|
|
147
|
+
fill = DEFAULT_FILL,
|
|
148
|
+
width = VIEW_WIDTH,
|
|
149
|
+
height = VIEW_HEIGHT,
|
|
150
|
+
...props
|
|
151
|
+
}: TypingDotsProps) => (
|
|
152
|
+
// No `overflow="visible"` despite the source setting it: a dot runs from
|
|
153
|
+
// y=0 at the top of its bounce to y=16.23 at the bottom of the overshoot,
|
|
154
|
+
// so nothing leaves the 48x20 box. Leaning on overflow would be the fragile
|
|
155
|
+
// choice — react-native-svg does not honour it consistently on Android.
|
|
156
|
+
<Svg
|
|
157
|
+
width={width}
|
|
158
|
+
height={height}
|
|
159
|
+
viewBox={`0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`}
|
|
160
|
+
fill="none"
|
|
161
|
+
{...props}
|
|
162
|
+
>
|
|
163
|
+
<G fill={fill}>
|
|
164
|
+
{DOT_X.map((_, index) => (
|
|
165
|
+
<Dot key={index} index={index} />
|
|
166
|
+
))}
|
|
167
|
+
</G>
|
|
168
|
+
</Svg>
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
TypingDots.displayName = 'TypingDots';
|
|
@@ -7,6 +7,7 @@ import OpenIMSDK, {
|
|
|
7
7
|
type ConversationItem,
|
|
8
8
|
type GroupApplicationItem,
|
|
9
9
|
type GroupItem,
|
|
10
|
+
type ConversationInputStatus,
|
|
10
11
|
type GroupMemberItem,
|
|
11
12
|
type MessageItem,
|
|
12
13
|
type MessagePinned,
|
|
@@ -25,7 +26,11 @@ import { useChatContext } from '../context';
|
|
|
25
26
|
import isEmpty from 'lodash/isEmpty';
|
|
26
27
|
import { markMessageRevoked, useMessageStore } from '../store/message';
|
|
27
28
|
import { syncPinnedMessagesOnEvent } from '../hooks/message/usePinMessage';
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
belongsToConversation,
|
|
31
|
+
conversationIdsOfMessage,
|
|
32
|
+
} from '../utils/message';
|
|
33
|
+
import { useInputStateStore } from '../store/inputState';
|
|
29
34
|
import { getConversationID } from '../utils/conversation';
|
|
30
35
|
import { clearSelfExitedGroup } from '../utils/groupExit';
|
|
31
36
|
import { DeviceEventEmitter } from 'react-native';
|
|
@@ -371,8 +376,34 @@ export const useChatListener = () => {
|
|
|
371
376
|
[updateOneMessage]
|
|
372
377
|
);
|
|
373
378
|
|
|
379
|
+
const onInputStatusChanged = useCallback(
|
|
380
|
+
(status: ConversationInputStatus) => {
|
|
381
|
+
// Never reflect the viewer's own composing back at them.
|
|
382
|
+
if (!status?.userID || status.userID === currentUserId) return;
|
|
383
|
+
|
|
384
|
+
// Stored for every conversation, not just the open one: the thread list
|
|
385
|
+
// shows typing for threads the user is not currently inside, and the
|
|
386
|
+
// store is keyed by conversation so each reader takes only its own.
|
|
387
|
+
useInputStateStore.getState().applyInputStatus(status);
|
|
388
|
+
},
|
|
389
|
+
[currentUserId]
|
|
390
|
+
);
|
|
391
|
+
|
|
374
392
|
const onRecvNewMessages = useCallback((messages: MessageItem[]) => {
|
|
375
393
|
// console.log('[Listener] OnRecvNewMessages', messages);
|
|
394
|
+
// A sender whose message just arrived has stopped composing. Runs over
|
|
395
|
+
// every message and above the guards below, because typing is tracked for
|
|
396
|
+
// all conversations rather than only the open one — and those guards bail
|
|
397
|
+
// outright while the user is sitting on the inbox.
|
|
398
|
+
const { removeTyper } = useInputStateStore.getState();
|
|
399
|
+
for (const message of messages) {
|
|
400
|
+
const senderId = message.sendID;
|
|
401
|
+
if (!senderId) continue;
|
|
402
|
+
conversationIdsOfMessage(message).forEach((id) =>
|
|
403
|
+
removeTyper(id, senderId)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
376
407
|
const conversationId =
|
|
377
408
|
useConversationStore.getState().currentConversationId;
|
|
378
409
|
const targetUserId = useConversationStore.getState().currentUserId;
|
|
@@ -415,6 +446,7 @@ export const useChatListener = () => {
|
|
|
415
446
|
OpenIMSDK.on(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
416
447
|
OpenIMSDK.on(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
417
448
|
OpenIMSDK.on(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
449
|
+
OpenIMSDK.on(OpenIMEvent.OnInputStatusChanged, onInputStatusChanged);
|
|
418
450
|
OpenIMSDK.on(
|
|
419
451
|
OpenIMEvent.OnRecvOfflineNewMessages,
|
|
420
452
|
onRecvOfflineNewMessages
|
|
@@ -444,6 +476,7 @@ export const useChatListener = () => {
|
|
|
444
476
|
// Message status
|
|
445
477
|
OpenIMSDK.off(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
|
|
446
478
|
OpenIMSDK.off(OpenIMEvent.OnRecvNewMessages, onRecvNewMessages);
|
|
479
|
+
OpenIMSDK.off(OpenIMEvent.OnInputStatusChanged, onInputStatusChanged);
|
|
447
480
|
OpenIMSDK.off(OpenIMEvent.OnNewRecvMessageRevoked, onMessageRevoked);
|
|
448
481
|
OpenIMSDK.off(OpenIMEvent.OnRecvMessagePinned, onMessagePinned);
|
|
449
482
|
OpenIMSDK.off(
|
|
@@ -461,6 +494,7 @@ export const useChatListener = () => {
|
|
|
461
494
|
onNewConversation,
|
|
462
495
|
onSendMessageProgress,
|
|
463
496
|
onRecvNewMessages,
|
|
497
|
+
onInputStatusChanged,
|
|
464
498
|
onRecvOfflineNewMessages,
|
|
465
499
|
OnGroupMemberInfoChanged,
|
|
466
500
|
onJoinedGroupDeleted,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import OpenIMSDK from '@droppii/openim-rn-client-sdk';
|
|
3
|
+
import { groupQueryKeys } from '../query-keys';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Display name of one group member.
|
|
7
|
+
*
|
|
8
|
+
* Fetches a single member rather than the whole roster: the only place this is
|
|
9
|
+
* needed is the typing sentence, which names at most one person — beyond that
|
|
10
|
+
* the copy collapses to "Nhiều người". Cached by react-query so a dot flickering
|
|
11
|
+
* on and off does not re-hit the SDK.
|
|
12
|
+
*/
|
|
13
|
+
export const useGroupMemberName = (
|
|
14
|
+
groupID?: string,
|
|
15
|
+
userID?: string
|
|
16
|
+
): string | undefined => {
|
|
17
|
+
const { data } = useQuery({
|
|
18
|
+
queryKey: groupQueryKeys.member(groupID, userID),
|
|
19
|
+
enabled: !!groupID && !!userID,
|
|
20
|
+
staleTime: 5 * 60_000,
|
|
21
|
+
queryFn: async () => {
|
|
22
|
+
const members = await OpenIMSDK.getSpecifiedGroupMembersInfo({
|
|
23
|
+
groupID: groupID!,
|
|
24
|
+
userIDList: [userID!],
|
|
25
|
+
});
|
|
26
|
+
return members?.[0]?.nickname ?? null;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return data ?? undefined;
|
|
31
|
+
};
|
package/src/hooks/query-keys.ts
CHANGED
|
@@ -83,4 +83,6 @@ export const groupQueryKeys = {
|
|
|
83
83
|
leave: () => [...groupQueryKeys.all, 'leave'] as const,
|
|
84
84
|
inviteInfo: (code: string) =>
|
|
85
85
|
[...groupQueryKeys.all, 'inviteInfo', code] as const,
|
|
86
|
+
member: (groupID?: string, userID?: string) =>
|
|
87
|
+
[...groupQueryKeys.all, 'member', groupID, userID] as const,
|
|
86
88
|
};
|