@planningcenter/chat-react-native 4.4.1-qa-staging.9 → 4.4.1-qa-staging.10
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/build/components/conversation/system_message.d.ts.map +1 -1
- package/build/components/conversation/system_message.js +85 -74
- package/build/components/conversation/system_message.js.map +1 -1
- package/build/components/conversations/conversation_preview.js +2 -0
- package/build/components/conversations/conversation_preview.js.map +1 -1
- package/build/types/jolt_events/message_events.d.ts +2 -0
- package/build/types/jolt_events/message_events.d.ts.map +1 -1
- package/build/types/jolt_events/message_events.js.map +1 -1
- package/build/types/resources/conversation.d.ts +1 -1
- package/build/types/resources/conversation.d.ts.map +1 -1
- package/build/types/resources/conversation.js.map +1 -1
- package/build/types/resources/message.d.ts +2 -0
- package/build/types/resources/message.d.ts.map +1 -1
- package/build/types/resources/message.js.map +1 -1
- package/build/utils/date.d.ts +1 -0
- package/build/utils/date.d.ts.map +1 -1
- package/build/utils/date.js +15 -1
- package/build/utils/date.js.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js +2 -1
- package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
- package/build/utils/request/messages_data_options.d.ts.map +1 -1
- package/build/utils/request/messages_data_options.js +1 -1
- package/build/utils/request/messages_data_options.js.map +1 -1
- package/build/utils/system_messages.d.ts +7 -5
- package/build/utils/system_messages.d.ts.map +1 -1
- package/build/utils/system_messages.js +10 -2
- package/build/utils/system_messages.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/utils/system_messages.ts +19 -32
- package/src/components/conversation/__tests__/system_message.test.tsx +104 -0
- package/src/components/conversation/system_message.tsx +20 -6
- package/src/components/conversations/__tests__/conversation_preview.test.tsx +70 -0
- package/src/components/conversations/conversation_preview.tsx +4 -0
- package/src/types/jolt_events/message_events.ts +2 -0
- package/src/types/resources/conversation.ts +1 -1
- package/src/types/resources/message.ts +3 -0
- package/src/utils/__tests__/date.test.ts +19 -0
- package/src/utils/__tests__/group_messages.test.ts +1 -0
- package/src/utils/date.ts +8 -1
- package/src/utils/jolt/__tests__/transform_message_event_data_to_message_resource.test.ts +35 -0
- package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +1 -0
- package/src/utils/request/messages_data_options.ts +1 -0
- package/src/utils/system_messages.ts +19 -10
package/build/utils/date.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { date as formatDate } from '@planningcenter/datetime-fmt';
|
|
1
|
+
import { date as formatDate, time as formatTime } from '@planningcenter/datetime-fmt';
|
|
2
2
|
import dayjs from './dayjs';
|
|
3
3
|
export function formatDatePreview(date) {
|
|
4
4
|
if (!date) return '';
|
|
@@ -65,4 +65,18 @@ export function relativeTime(date) {
|
|
|
65
65
|
return dayjs(date).format('h:mma');
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
export function formatEventDateTime(date) {
|
|
69
|
+
const {
|
|
70
|
+
isThisYear
|
|
71
|
+
} = getRelativeDateStatus(date);
|
|
72
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
73
|
+
const day = formatDate(date, {
|
|
74
|
+
style: 'abbreviated',
|
|
75
|
+
year: !isThisYear,
|
|
76
|
+
timeZone
|
|
77
|
+
});
|
|
78
|
+
return `${day} at ${formatTime(date, {
|
|
79
|
+
timeZone
|
|
80
|
+
})}`;
|
|
81
|
+
}
|
|
68
82
|
//# sourceMappingURL=date.js.map
|
package/build/utils/date.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date.js","names":["date","formatDate","dayjs","formatDatePreview","now","isToday","isSame","isThisWeek","isThisYear","format","style","formatShorterDate","year","replace","getRelativeDateStatus","relativeDateTime","dateTimeString","Date","toDateString","getFullYear","toLocaleTimeString","hour","minute","hour12","toLocaleDateString","month","day","relativeTime","then","duration","diff","asDays","fromNow"],"sources":["../../src/utils/date.ts"],"sourcesContent":["import { date as formatDate } from '@planningcenter/datetime-fmt'\nimport dayjs from './dayjs'\n\nexport type DateProps = string | number | Date\n\nexport function formatDatePreview(date?: DateProps) {\n if (!date) return ''\n\n const now = dayjs()\n const isToday = now.isSame(date, 'day')\n const isThisWeek = now.isSame(date, 'week')\n const isThisYear = now.isSame(date, 'year')\n\n if (isToday) return dayjs(date).format('h:mma')\n if (isThisWeek) return formatDate(date, { style: 'relative-short' })\n if (isThisYear) return formatDate(date, { style: 'abbreviated' })\n\n // TODO: Org date format\n return formatShorterDate(date)\n}\n\nconst formatShorterDate = (date: DateProps) => {\n // Drop the century from the year\n return formatDate(date, { style: 'short', year: true }).replace(/20(\\d{2})/, '$1')\n}\n\nexport function getRelativeDateStatus(date: DateProps) {\n const now = dayjs()\n\n const isToday = now.isSame(date, 'day')\n const isThisWeek = now.isSame(date, 'week')\n const isThisYear = now.isSame(date, 'year')\n\n return { isToday, isThisWeek, isThisYear }\n}\n\nexport function relativeDateTime(dateTimeString: string): string {\n const date = new Date(dateTimeString)\n const now = new Date()\n\n const isToday = date.toDateString() === now.toDateString()\n const isThisYear = date.getFullYear() === now.getFullYear()\n\n if (isToday) {\n return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n } else if (isThisYear) {\n return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n } else {\n return date.toLocaleDateString('en-US')\n }\n}\n\nexport function relativeTime(date: DateProps): string {\n const now = dayjs()\n const then = dayjs(date)\n const duration = dayjs.duration(now.diff(then))\n\n if (duration.asDays() < 1) {\n return then.fromNow()\n } else {\n return dayjs(date).format('h:mma')\n }\n}\n"],"mappings":"AAAA,SAASA,IAAI,IAAIC,UAAU,QAAQ,8BAA8B;
|
|
1
|
+
{"version":3,"file":"date.js","names":["date","formatDate","time","formatTime","dayjs","formatDatePreview","now","isToday","isSame","isThisWeek","isThisYear","format","style","formatShorterDate","year","replace","getRelativeDateStatus","relativeDateTime","dateTimeString","Date","toDateString","getFullYear","toLocaleTimeString","hour","minute","hour12","toLocaleDateString","month","day","relativeTime","then","duration","diff","asDays","fromNow","formatEventDateTime","timeZone","Intl","DateTimeFormat","resolvedOptions"],"sources":["../../src/utils/date.ts"],"sourcesContent":["import { date as formatDate, time as formatTime } from '@planningcenter/datetime-fmt'\nimport dayjs from './dayjs'\n\nexport type DateProps = string | number | Date\n\nexport function formatDatePreview(date?: DateProps) {\n if (!date) return ''\n\n const now = dayjs()\n const isToday = now.isSame(date, 'day')\n const isThisWeek = now.isSame(date, 'week')\n const isThisYear = now.isSame(date, 'year')\n\n if (isToday) return dayjs(date).format('h:mma')\n if (isThisWeek) return formatDate(date, { style: 'relative-short' })\n if (isThisYear) return formatDate(date, { style: 'abbreviated' })\n\n // TODO: Org date format\n return formatShorterDate(date)\n}\n\nconst formatShorterDate = (date: DateProps) => {\n // Drop the century from the year\n return formatDate(date, { style: 'short', year: true }).replace(/20(\\d{2})/, '$1')\n}\n\nexport function getRelativeDateStatus(date: DateProps) {\n const now = dayjs()\n\n const isToday = now.isSame(date, 'day')\n const isThisWeek = now.isSame(date, 'week')\n const isThisYear = now.isSame(date, 'year')\n\n return { isToday, isThisWeek, isThisYear }\n}\n\nexport function relativeDateTime(dateTimeString: string): string {\n const date = new Date(dateTimeString)\n const now = new Date()\n\n const isToday = date.toDateString() === now.toDateString()\n const isThisYear = date.getFullYear() === now.getFullYear()\n\n if (isToday) {\n return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })\n } else if (isThisYear) {\n return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })\n } else {\n return date.toLocaleDateString('en-US')\n }\n}\n\nexport function relativeTime(date: DateProps): string {\n const now = dayjs()\n const then = dayjs(date)\n const duration = dayjs.duration(now.diff(then))\n\n if (duration.asDays() < 1) {\n return then.fromNow()\n } else {\n return dayjs(date).format('h:mma')\n }\n}\n\nexport function formatEventDateTime(date: DateProps) {\n const { isThisYear } = getRelativeDateStatus(date)\n const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone\n const day = formatDate(date, { style: 'abbreviated', year: !isThisYear, timeZone })\n return `${day} at ${formatTime(date, { timeZone })}`\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform_message_event_data_to_message_resource.d.ts","sourceRoot":"","sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAI5E,wBAAgB,0CAA0C,CAAC,EACzD,IAAI,EACJ,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"transform_message_event_data_to_message_resource.d.ts","sourceRoot":"","sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAI5E,wBAAgB,0CAA0C,CAAC,EACzD,IAAI,EACJ,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,eAAe,CAgClB"}
|
|
@@ -29,7 +29,8 @@ export function transformMessageEventDataToMessageResource({
|
|
|
29
29
|
names: data.system_text_parts.names,
|
|
30
30
|
overflowCount: data.system_text_parts.overflow_count,
|
|
31
31
|
action: data.system_text_parts.action
|
|
32
|
-
} : null
|
|
32
|
+
} : null,
|
|
33
|
+
systemTextFormat: data.system_text_format
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
//# sourceMappingURL=transform_message_event_data_to_message_resource.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform_message_event_data_to_message_resource.js","names":["deepCamelCaseKeys","transformMessageEventDataToMessageResource","data","currentPersonId","type","id","sort_key","text","html","createdAt","created_at","deletedAt","deleted_at","textEditedAt","text_edited_at","mine","author_id","attachments","author","name","author_name","avatar","author_avatar","reactionCounts","replyCount","reply_count","replyRootId","reply_root_id","idempotentKey","idempotent_key","messageType","message_type","personIdsForSystemEvent","system_event_person_ids","systemTextParts","system_text_parts","names","overflowCount","overflow_count","action"],"sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"sourcesContent":["import { MessageResource } from '../../types'\nimport { MessageCreatedEvent } from '../../types/jolt_events/message_events'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { deepCamelCaseKeys } from '../deepCamelCaseKeys'\n\nexport function transformMessageEventDataToMessageResource({\n data,\n currentPersonId,\n}: {\n data: MessageCreatedEvent['data']['data']\n currentPersonId: number\n}): MessageResource {\n return {\n type: 'Message',\n id: data.sort_key,\n text: data.text,\n html: data.html,\n createdAt: data.created_at,\n deletedAt: data.deleted_at,\n textEditedAt: data.text_edited_at,\n mine: data.author_id === currentPersonId,\n attachments: deepCamelCaseKeys<DenormalizedAttachmentResource[]>(data.attachments) || [],\n author: {\n type: 'Person',\n id: data.author_id,\n name: data.author_name,\n avatar: data.author_avatar,\n },\n reactionCounts: [],\n replyCount: data.reply_count || 0,\n replyRootId: data.reply_root_id || null,\n idempotentKey: data.idempotent_key ?? null,\n messageType: data.message_type,\n personIdsForSystemEvent: data.system_event_person_ids,\n systemTextParts: data.system_text_parts\n ? {\n names: data.system_text_parts.names,\n overflowCount: data.system_text_parts.overflow_count,\n action: data.system_text_parts.action,\n }\n : null,\n }\n}\n"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,sBAAsB;AAExD,OAAO,SAASC,0CAA0CA,CAAC;EACzDC,IAAI;EACJC;AAIF,CAAC,EAAmB;EAClB,OAAO;IACLC,IAAI,EAAE,SAAS;IACfC,EAAE,EAAEH,IAAI,CAACI,QAAQ;IACjBC,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,IAAI,EAAEN,IAAI,CAACM,IAAI;IACfC,SAAS,EAAEP,IAAI,CAACQ,UAAU;IAC1BC,SAAS,EAAET,IAAI,CAACU,UAAU;IAC1BC,YAAY,EAAEX,IAAI,CAACY,cAAc;IACjCC,IAAI,EAAEb,IAAI,CAACc,SAAS,KAAKb,eAAe;IACxCc,WAAW,EAAEjB,iBAAiB,CAAmCE,IAAI,CAACe,WAAW,CAAC,IAAI,EAAE;IACxFC,MAAM,EAAE;MACNd,IAAI,EAAE,QAAQ;MACdC,EAAE,EAAEH,IAAI,CAACc,SAAS;MAClBG,IAAI,EAAEjB,IAAI,CAACkB,WAAW;MACtBC,MAAM,EAAEnB,IAAI,CAACoB;IACf,CAAC;IACDC,cAAc,EAAE,EAAE;IAClBC,UAAU,EAAEtB,IAAI,CAACuB,WAAW,IAAI,CAAC;IACjCC,WAAW,EAAExB,IAAI,CAACyB,aAAa,IAAI,IAAI;IACvCC,aAAa,EAAE1B,IAAI,CAAC2B,cAAc,IAAI,IAAI;IAC1CC,WAAW,EAAE5B,IAAI,CAAC6B,YAAY;IAC9BC,uBAAuB,EAAE9B,IAAI,CAAC+B,uBAAuB;IACrDC,eAAe,EAAEhC,IAAI,CAACiC,iBAAiB,GACnC;MACEC,KAAK,EAAElC,IAAI,CAACiC,iBAAiB,CAACC,KAAK;MACnCC,aAAa,EAAEnC,IAAI,CAACiC,iBAAiB,CAACG,cAAc;MACpDC,MAAM,EAAErC,IAAI,CAACiC,iBAAiB,CAACI;IACjC,CAAC,GACD;
|
|
1
|
+
{"version":3,"file":"transform_message_event_data_to_message_resource.js","names":["deepCamelCaseKeys","transformMessageEventDataToMessageResource","data","currentPersonId","type","id","sort_key","text","html","createdAt","created_at","deletedAt","deleted_at","textEditedAt","text_edited_at","mine","author_id","attachments","author","name","author_name","avatar","author_avatar","reactionCounts","replyCount","reply_count","replyRootId","reply_root_id","idempotentKey","idempotent_key","messageType","message_type","personIdsForSystemEvent","system_event_person_ids","systemTextParts","system_text_parts","names","overflowCount","overflow_count","action","systemTextFormat","system_text_format"],"sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"sourcesContent":["import { MessageResource } from '../../types'\nimport { MessageCreatedEvent } from '../../types/jolt_events/message_events'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { deepCamelCaseKeys } from '../deepCamelCaseKeys'\n\nexport function transformMessageEventDataToMessageResource({\n data,\n currentPersonId,\n}: {\n data: MessageCreatedEvent['data']['data']\n currentPersonId: number\n}): MessageResource {\n return {\n type: 'Message',\n id: data.sort_key,\n text: data.text,\n html: data.html,\n createdAt: data.created_at,\n deletedAt: data.deleted_at,\n textEditedAt: data.text_edited_at,\n mine: data.author_id === currentPersonId,\n attachments: deepCamelCaseKeys<DenormalizedAttachmentResource[]>(data.attachments) || [],\n author: {\n type: 'Person',\n id: data.author_id,\n name: data.author_name,\n avatar: data.author_avatar,\n },\n reactionCounts: [],\n replyCount: data.reply_count || 0,\n replyRootId: data.reply_root_id || null,\n idempotentKey: data.idempotent_key ?? null,\n messageType: data.message_type,\n personIdsForSystemEvent: data.system_event_person_ids,\n systemTextParts: data.system_text_parts\n ? {\n names: data.system_text_parts.names,\n overflowCount: data.system_text_parts.overflow_count,\n action: data.system_text_parts.action,\n }\n : null,\n systemTextFormat: data.system_text_format,\n }\n}\n"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,sBAAsB;AAExD,OAAO,SAASC,0CAA0CA,CAAC;EACzDC,IAAI;EACJC;AAIF,CAAC,EAAmB;EAClB,OAAO;IACLC,IAAI,EAAE,SAAS;IACfC,EAAE,EAAEH,IAAI,CAACI,QAAQ;IACjBC,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,IAAI,EAAEN,IAAI,CAACM,IAAI;IACfC,SAAS,EAAEP,IAAI,CAACQ,UAAU;IAC1BC,SAAS,EAAET,IAAI,CAACU,UAAU;IAC1BC,YAAY,EAAEX,IAAI,CAACY,cAAc;IACjCC,IAAI,EAAEb,IAAI,CAACc,SAAS,KAAKb,eAAe;IACxCc,WAAW,EAAEjB,iBAAiB,CAAmCE,IAAI,CAACe,WAAW,CAAC,IAAI,EAAE;IACxFC,MAAM,EAAE;MACNd,IAAI,EAAE,QAAQ;MACdC,EAAE,EAAEH,IAAI,CAACc,SAAS;MAClBG,IAAI,EAAEjB,IAAI,CAACkB,WAAW;MACtBC,MAAM,EAAEnB,IAAI,CAACoB;IACf,CAAC;IACDC,cAAc,EAAE,EAAE;IAClBC,UAAU,EAAEtB,IAAI,CAACuB,WAAW,IAAI,CAAC;IACjCC,WAAW,EAAExB,IAAI,CAACyB,aAAa,IAAI,IAAI;IACvCC,aAAa,EAAE1B,IAAI,CAAC2B,cAAc,IAAI,IAAI;IAC1CC,WAAW,EAAE5B,IAAI,CAAC6B,YAAY;IAC9BC,uBAAuB,EAAE9B,IAAI,CAAC+B,uBAAuB;IACrDC,eAAe,EAAEhC,IAAI,CAACiC,iBAAiB,GACnC;MACEC,KAAK,EAAElC,IAAI,CAACiC,iBAAiB,CAACC,KAAK;MACnCC,aAAa,EAAEnC,IAAI,CAACiC,iBAAiB,CAACG,cAAc;MACpDC,MAAM,EAAErC,IAAI,CAACiC,iBAAiB,CAACI;IACjC,CAAC,GACD,IAAI;IACRC,gBAAgB,EAAEtC,IAAI,CAACuC;EACzB,CAAC;AACH","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages_data_options.d.ts","sourceRoot":"","sources":["../../../src/utils/request/messages_data_options.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;
|
|
1
|
+
{"version":3,"file":"messages_data_options.d.ts","sourceRoot":"","sources":["../../../src/utils/request/messages_data_options.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;CAoB5B,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAmE,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const getMessageFields = {
|
|
2
|
-
Message: ['text', 'text_edited_at', 'mine', 'attachments', 'created_at', 'deleted_at', 'author', 'reaction_counts', 'reply_count', 'reply_root', 'idempotent_key', 'message_type', 'person_ids_for_system_event', 'system_text_parts'],
|
|
2
|
+
Message: ['text', 'text_edited_at', 'mine', 'attachments', 'created_at', 'deleted_at', 'author', 'reaction_counts', 'reply_count', 'reply_root', 'idempotent_key', 'message_type', 'person_ids_for_system_event', 'system_text_parts', 'system_text_format'],
|
|
3
3
|
Person: ['name', 'avatar'],
|
|
4
4
|
ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids']
|
|
5
5
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages_data_options.js","names":["getMessageFields","Message","Person","ReactionCount","getMessagesInclude"],"sources":["../../../src/utils/request/messages_data_options.ts"],"sourcesContent":["export const getMessageFields = {\n Message: [\n 'text',\n 'text_edited_at',\n 'mine',\n 'attachments',\n 'created_at',\n 'deleted_at',\n 'author',\n 'reaction_counts',\n 'reply_count',\n 'reply_root',\n 'idempotent_key',\n 'message_type',\n 'person_ids_for_system_event',\n 'system_text_parts',\n ],\n Person: ['name', 'avatar'],\n ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],\n}\n\nexport const getMessagesInclude = ['author', 'reaction_counts', 'reply_root', 'reply_root.author']\n"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,GAAG;EAC9BC,OAAO,EAAE,CACP,MAAM,EACN,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,6BAA6B,EAC7B,mBAAmB,
|
|
1
|
+
{"version":3,"file":"messages_data_options.js","names":["getMessageFields","Message","Person","ReactionCount","getMessagesInclude"],"sources":["../../../src/utils/request/messages_data_options.ts"],"sourcesContent":["export const getMessageFields = {\n Message: [\n 'text',\n 'text_edited_at',\n 'mine',\n 'attachments',\n 'created_at',\n 'deleted_at',\n 'author',\n 'reaction_counts',\n 'reply_count',\n 'reply_root',\n 'idempotent_key',\n 'message_type',\n 'person_ids_for_system_event',\n 'system_text_parts',\n 'system_text_format',\n ],\n Person: ['name', 'avatar'],\n ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],\n}\n\nexport const getMessagesInclude = ['author', 'reaction_counts', 'reply_root', 'reply_root.author']\n"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,GAAG;EAC9BC,OAAO,EAAE,CACP,MAAM,EACN,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,6BAA6B,EAC7B,mBAAmB,EACnB,oBAAoB,CACrB;EACDC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC;EAC1BC,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY;AACtE,CAAC;AAED,OAAO,MAAMC,kBAAkB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,CAAC","ignoreList":[]}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { MessageResource } from '../types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export interface SystemMessageResource extends MessageResource {
|
|
5
|
-
messageType: SystemMessageType;
|
|
2
|
+
export interface PeopleSystemMessageResource extends MessageResource {
|
|
3
|
+
systemTextFormat: 'people';
|
|
6
4
|
systemTextParts: {
|
|
7
5
|
names: string[];
|
|
8
6
|
overflowCount: number;
|
|
@@ -10,6 +8,10 @@ export interface SystemMessageResource extends MessageResource {
|
|
|
10
8
|
};
|
|
11
9
|
personIdsForSystemEvent: number[];
|
|
12
10
|
}
|
|
11
|
+
export interface ActorSystemMessageResource extends MessageResource {
|
|
12
|
+
systemTextFormat: 'actor';
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
export type SystemMessageResource = PeopleSystemMessageResource | ActorSystemMessageResource;
|
|
13
16
|
export declare const isSystemMessage: (message: MessageResource) => message is SystemMessageResource;
|
|
14
|
-
export {};
|
|
15
17
|
//# sourceMappingURL=system_messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system_messages.d.ts","sourceRoot":"","sources":["../../src/utils/system_messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,
|
|
1
|
+
{"version":3,"file":"system_messages.d.ts","sourceRoot":"","sources":["../../src/utils/system_messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,MAAM,WAAW,2BAA4B,SAAQ,eAAe;IAClE,gBAAgB,EAAE,QAAQ,CAAA;IAC1B,eAAe,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3E,uBAAuB,EAAE,MAAM,EAAE,CAAA;CAClC;AAED,MAAM,WAAW,0BAA2B,SAAQ,eAAe;IACjE,gBAAgB,EAAE,OAAO,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,qBAAqB,GAAG,2BAA2B,GAAG,0BAA0B,CAAA;AAE5F,eAAO,MAAM,eAAe,GAAI,SAAS,eAAe,KAAG,OAAO,IAAI,qBASrE,CAAA"}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
export const isSystemMessage = message => {
|
|
2
|
+
switch (message.systemTextFormat) {
|
|
3
|
+
case 'people':
|
|
4
|
+
return message.systemTextParts != null && message.personIdsForSystemEvent != null;
|
|
5
|
+
case 'actor':
|
|
6
|
+
return !!message.text;
|
|
7
|
+
default:
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
3
11
|
//# sourceMappingURL=system_messages.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system_messages.js","names":["
|
|
1
|
+
{"version":3,"file":"system_messages.js","names":["isSystemMessage","message","systemTextFormat","systemTextParts","personIdsForSystemEvent","text"],"sources":["../../src/utils/system_messages.ts"],"sourcesContent":["import type { MessageResource } from '../types'\n\nexport interface PeopleSystemMessageResource extends MessageResource {\n systemTextFormat: 'people'\n systemTextParts: { names: string[]; overflowCount: number; action: string }\n personIdsForSystemEvent: number[]\n}\n\nexport interface ActorSystemMessageResource extends MessageResource {\n systemTextFormat: 'actor'\n text: string\n}\n\nexport type SystemMessageResource = PeopleSystemMessageResource | ActorSystemMessageResource\n\nexport const isSystemMessage = (message: MessageResource): message is SystemMessageResource => {\n switch (message.systemTextFormat) {\n case 'people':\n return message.systemTextParts != null && message.personIdsForSystemEvent != null\n case 'actor':\n return !!message.text\n default:\n return false\n }\n}\n"],"mappings":"AAeA,OAAO,MAAMA,eAAe,GAAIC,OAAwB,IAAuC;EAC7F,QAAQA,OAAO,CAACC,gBAAgB;IAC9B,KAAK,QAAQ;MACX,OAAOD,OAAO,CAACE,eAAe,IAAI,IAAI,IAAIF,OAAO,CAACG,uBAAuB,IAAI,IAAI;IACnF,KAAK,OAAO;MACV,OAAO,CAAC,CAACH,OAAO,CAACI,IAAI;IACvB;MACE,OAAO,KAAK;EAChB;AACF,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "4.4.1-qa-staging.
|
|
3
|
+
"version": "4.4.1-qa-staging.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
|
35
35
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
|
36
|
-
"@planningcenter/emoji-keyboard": "4.4.1-qa-staging.
|
|
36
|
+
"@planningcenter/emoji-keyboard": "4.4.1-qa-staging.10",
|
|
37
37
|
"lodash-inflection": "^1.5.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"sf-symbols-typescript": "^2.2.0",
|
|
85
85
|
"typescript": "~6.0.3"
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "1e3d2b7deafd780d4651c8da8adc7083ff348c3d"
|
|
88
88
|
}
|
|
@@ -18,62 +18,49 @@ const makeMessage = (overrides: Partial<MessageResource> = {}): MessageResource
|
|
|
18
18
|
messageType: 'user',
|
|
19
19
|
personIdsForSystemEvent: null,
|
|
20
20
|
systemTextParts: null,
|
|
21
|
+
systemTextFormat: null,
|
|
21
22
|
...overrides,
|
|
22
23
|
})
|
|
23
24
|
|
|
25
|
+
const joinedParts = { names: ['Alice'], overflowCount: 0, action: 'joined the conversation' }
|
|
26
|
+
|
|
24
27
|
describe('isSystemMessage', () => {
|
|
25
|
-
it('returns true for
|
|
28
|
+
it('returns true for people messages with names and person ids', () => {
|
|
26
29
|
const message = makeMessage({
|
|
27
30
|
messageType: 'user_joined',
|
|
31
|
+
systemTextFormat: 'people',
|
|
28
32
|
personIdsForSystemEvent: [1],
|
|
29
|
-
systemTextParts:
|
|
33
|
+
systemTextParts: joinedParts,
|
|
30
34
|
})
|
|
31
35
|
expect(isSystemMessage(message)).toBe(true)
|
|
32
36
|
})
|
|
33
37
|
|
|
34
|
-
it('
|
|
38
|
+
it('returns true for actor messages of a type the client does not know', () => {
|
|
35
39
|
const message = makeMessage({
|
|
36
|
-
messageType: '
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
names: ['Alice', 'Bob'],
|
|
40
|
-
overflowCount: 0,
|
|
41
|
-
action: 'joined the conversation',
|
|
42
|
-
},
|
|
40
|
+
messageType: 'some_future_type',
|
|
41
|
+
systemTextFormat: 'actor',
|
|
42
|
+
text: 'This conversation was archived by Alice',
|
|
43
43
|
})
|
|
44
|
-
|
|
45
|
-
expect(message.systemTextParts.names).toEqual(['Alice', 'Bob'])
|
|
46
|
-
expect(message.personIdsForSystemEvent).toEqual([1, 2])
|
|
47
|
-
} else {
|
|
48
|
-
throw new Error('Expected isSystemMessage to return true')
|
|
49
|
-
}
|
|
44
|
+
expect(isSystemMessage(message)).toBe(true)
|
|
50
45
|
})
|
|
51
46
|
|
|
52
|
-
it('returns false for
|
|
53
|
-
const message = makeMessage({ messageType: '
|
|
47
|
+
it('returns false for messages without a system text format', () => {
|
|
48
|
+
const message = makeMessage({ messageType: 'user_joined', systemTextParts: joinedParts })
|
|
54
49
|
expect(isSystemMessage(message)).toBe(false)
|
|
55
50
|
})
|
|
56
51
|
|
|
57
|
-
it('returns false for
|
|
58
|
-
const message = makeMessage({
|
|
52
|
+
it('returns false for people messages without systemTextParts', () => {
|
|
53
|
+
const message = makeMessage({ systemTextFormat: 'people', personIdsForSystemEvent: [1] })
|
|
59
54
|
expect(isSystemMessage(message)).toBe(false)
|
|
60
55
|
})
|
|
61
56
|
|
|
62
|
-
it('returns false
|
|
63
|
-
const message = makeMessage({
|
|
64
|
-
messageType: 'user_joined',
|
|
65
|
-
personIdsForSystemEvent: [1],
|
|
66
|
-
systemTextParts: null,
|
|
67
|
-
})
|
|
57
|
+
it('returns false for people messages without personIdsForSystemEvent', () => {
|
|
58
|
+
const message = makeMessage({ systemTextFormat: 'people', systemTextParts: joinedParts })
|
|
68
59
|
expect(isSystemMessage(message)).toBe(false)
|
|
69
60
|
})
|
|
70
61
|
|
|
71
|
-
it('returns false
|
|
72
|
-
const message = makeMessage({
|
|
73
|
-
messageType: 'user_joined',
|
|
74
|
-
personIdsForSystemEvent: null,
|
|
75
|
-
systemTextParts: { names: ['Alice'], overflowCount: 0, action: 'joined the conversation' },
|
|
76
|
-
})
|
|
62
|
+
it.each([null, ''])('returns false for actor messages with text %p', text => {
|
|
63
|
+
const message = makeMessage({ systemTextFormat: 'actor', text })
|
|
77
64
|
expect(isSystemMessage(message)).toBe(false)
|
|
78
65
|
})
|
|
79
66
|
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { render } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../../__utils__/query_client'
|
|
5
|
+
import { ChatProvider } from '../../../contexts/chat_context'
|
|
6
|
+
import type { PersonResource } from '../../../types/resources/person'
|
|
7
|
+
import type { SystemMessageResource } from '../../../utils/system_messages'
|
|
8
|
+
import { SystemMessage } from '../system_message'
|
|
9
|
+
|
|
10
|
+
jest.mock('@react-navigation/native', () => ({
|
|
11
|
+
useNavigation: () => ({ navigate: jest.fn() }),
|
|
12
|
+
}))
|
|
13
|
+
jest.mock('../../../hooks/use_animated_message_background_color', () => ({
|
|
14
|
+
useAnimatedMessageBackgroundColor: () => ({
|
|
15
|
+
animatedBackgroundColor: {},
|
|
16
|
+
handleMessagePressIn: jest.fn(),
|
|
17
|
+
handleMessagePressOut: jest.fn(),
|
|
18
|
+
}),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
const author = { id: 1, type: 'Person', name: 'Wes', avatar: null } as unknown as PersonResource
|
|
22
|
+
|
|
23
|
+
const systemMessage = (overrides: Partial<SystemMessageResource>): SystemMessageResource =>
|
|
24
|
+
({
|
|
25
|
+
type: 'Message',
|
|
26
|
+
id: '01',
|
|
27
|
+
text: null,
|
|
28
|
+
html: '',
|
|
29
|
+
createdAt: '2026-02-03T14:18:00',
|
|
30
|
+
deletedAt: null,
|
|
31
|
+
textEditedAt: null,
|
|
32
|
+
mine: false,
|
|
33
|
+
attachments: [],
|
|
34
|
+
author,
|
|
35
|
+
reactionCounts: [],
|
|
36
|
+
replyCount: 0,
|
|
37
|
+
replyRootId: null,
|
|
38
|
+
messageType: 'user_joined',
|
|
39
|
+
personIdsForSystemEvent: null,
|
|
40
|
+
systemTextParts: null,
|
|
41
|
+
...overrides,
|
|
42
|
+
}) as SystemMessageResource
|
|
43
|
+
|
|
44
|
+
let queryClient: QueryClient
|
|
45
|
+
|
|
46
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
47
|
+
<QueryClientProvider client={queryClient}>
|
|
48
|
+
<ChatProvider
|
|
49
|
+
value={{
|
|
50
|
+
token: {
|
|
51
|
+
access_token: '',
|
|
52
|
+
created_at: 0,
|
|
53
|
+
expires_in: 0,
|
|
54
|
+
scope: '',
|
|
55
|
+
refresh_token: '',
|
|
56
|
+
token_type: '',
|
|
57
|
+
},
|
|
58
|
+
onUnauthorizedResponse: () => {},
|
|
59
|
+
theme: { theme: { colors: { interaction: 'rgb(0, 0, 255)' } } },
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</ChatProvider>
|
|
64
|
+
</QueryClientProvider>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const renderSystemMessage = (message: SystemMessageResource) =>
|
|
68
|
+
render(<SystemMessage message={message} conversationId={1} />, { wrapper: Wrapper })
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
jest.useFakeTimers({ now: new Date(2026, 5, 15, 12, 0) })
|
|
72
|
+
queryClient = buildTestQueryClient()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
queryClient.clear()
|
|
77
|
+
jest.useRealTimers()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('SystemMessage', () => {
|
|
81
|
+
it('renders actor messages as their text followed by the local date and time', () => {
|
|
82
|
+
const view = renderSystemMessage(
|
|
83
|
+
systemMessage({
|
|
84
|
+
messageType: 'conversation_archived',
|
|
85
|
+
systemTextFormat: 'actor',
|
|
86
|
+
text: 'This conversation was archived by Wes',
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
expect(view.getByText('This conversation was archived by Wes on Feb 3 at 2:18pm')).toBeTruthy()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('builds people messages from their names', () => {
|
|
94
|
+
const view = renderSystemMessage(
|
|
95
|
+
systemMessage({
|
|
96
|
+
systemTextFormat: 'people',
|
|
97
|
+
systemTextParts: { names: ['Ann', 'Bo'], overflowCount: 0, action: 'joined' },
|
|
98
|
+
personIdsForSystemEvent: [1, 2],
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
expect(view.getByText('Ann and Bo joined')).toBeTruthy()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -4,6 +4,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
|
|
|
4
4
|
import Animated from 'react-native-reanimated'
|
|
5
5
|
import { useAnimatedMessageBackgroundColor, useTheme } from '../../hooks'
|
|
6
6
|
import { ReactionCountResource } from '../../types/resources/reaction'
|
|
7
|
+
import { formatEventDateTime } from '../../utils/date'
|
|
7
8
|
import { Haptic } from '../../utils/native_adapters'
|
|
8
9
|
import { SystemMessageResource } from '../../utils/system_messages'
|
|
9
10
|
import { Text } from '../display'
|
|
@@ -15,21 +16,23 @@ interface SystemMessageProps {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function SystemMessage({ message, conversationId }: SystemMessageProps) {
|
|
18
|
-
const { reactionCounts
|
|
19
|
-
const { names, overflowCount, action } = systemTextParts
|
|
19
|
+
const { reactionCounts } = message
|
|
20
20
|
const styles = useStyles()
|
|
21
21
|
const navigation = useNavigation()
|
|
22
22
|
const { colors } = useTheme()
|
|
23
|
-
const text = systemMessageText(
|
|
24
|
-
const isTruncated =
|
|
23
|
+
const text = systemMessageText(message)
|
|
24
|
+
const isTruncated =
|
|
25
|
+
message.systemTextFormat === 'people' && message.systemTextParts.overflowCount > 0
|
|
25
26
|
const hasReactions = reactionCounts.length > 0
|
|
26
27
|
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
27
28
|
useAnimatedMessageBackgroundColor()
|
|
28
29
|
|
|
29
30
|
const handlePress = () => {
|
|
31
|
+
if (message.systemTextFormat !== 'people') return
|
|
32
|
+
|
|
30
33
|
navigation.navigate('SystemMessagePeople', {
|
|
31
34
|
conversation_id: conversationId,
|
|
32
|
-
person_ids: personIdsForSystemEvent,
|
|
35
|
+
person_ids: message.personIdsForSystemEvent,
|
|
33
36
|
})
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -87,7 +90,18 @@ export function SystemMessage({ message, conversationId }: SystemMessageProps) {
|
|
|
87
90
|
)
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
function systemMessageText(
|
|
93
|
+
function systemMessageText(message: SystemMessageResource): string {
|
|
94
|
+
switch (message.systemTextFormat) {
|
|
95
|
+
case 'people': {
|
|
96
|
+
const { names, overflowCount, action } = message.systemTextParts
|
|
97
|
+
return peopleSystemMessageText(names, overflowCount, action)
|
|
98
|
+
}
|
|
99
|
+
case 'actor':
|
|
100
|
+
return `${message.text} on ${formatEventDateTime(message.createdAt)}`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function peopleSystemMessageText(names: string[], overflowCount: number, action: string): string {
|
|
91
105
|
if (overflowCount > 0) {
|
|
92
106
|
const overflowLabel = `${overflowCount} ${overflowCount === 1 ? 'other' : 'others'}`
|
|
93
107
|
return `${names.join(', ')}, and ${overflowLabel} ${action}`
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { render } from '@testing-library/react-native'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { ConversationPreviewData, ConversationResource } from '../../../types'
|
|
4
|
+
import { formatDatePreview } from '../../../utils/date'
|
|
5
|
+
import { ConversationPreview } from '../conversation_preview'
|
|
6
|
+
|
|
7
|
+
jest.mock('../conversation_actions', () => ({
|
|
8
|
+
ConversationActions: ({
|
|
9
|
+
children,
|
|
10
|
+
accessibilityLabel,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
accessibilityLabel?: string
|
|
14
|
+
}) => {
|
|
15
|
+
const { View: MockView } = jest.requireActual('react-native')
|
|
16
|
+
return <MockView accessibilityLabel={accessibilityLabel}>{children}</MockView>
|
|
17
|
+
},
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const createdAt = '2026-03-04T15:30:00Z'
|
|
21
|
+
|
|
22
|
+
const conversation = (conversationPreview: ConversationPreviewData): ConversationResource => ({
|
|
23
|
+
type: 'Conversation',
|
|
24
|
+
id: 7,
|
|
25
|
+
title: 'Worship Team',
|
|
26
|
+
conversationPreview,
|
|
27
|
+
createdAt,
|
|
28
|
+
lastMessageCreatedAt: createdAt,
|
|
29
|
+
muted: false,
|
|
30
|
+
repliesDisabled: false,
|
|
31
|
+
unreadCount: 0,
|
|
32
|
+
unreadReaction: false,
|
|
33
|
+
updatedAt: createdAt,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const renderPreview = (preview: ConversationPreviewData) =>
|
|
37
|
+
render(<ConversationPreview conversation={conversation(preview)} onPress={jest.fn()} />)
|
|
38
|
+
|
|
39
|
+
describe('ConversationPreview', () => {
|
|
40
|
+
it('prefixes message previews with the author', () => {
|
|
41
|
+
const view = renderPreview({
|
|
42
|
+
kind: 'message',
|
|
43
|
+
authorName: 'Ada Lovelace',
|
|
44
|
+
value: null,
|
|
45
|
+
text: 'See you Sunday',
|
|
46
|
+
createdAt,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(view.getByText('Ada Lovelace: See you Sunday')).toBeTruthy()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('shows system previews as their text alone', () => {
|
|
53
|
+
const text = 'This conversation was unarchived by Ada Lovelace'
|
|
54
|
+
const view = renderPreview({
|
|
55
|
+
kind: 'system',
|
|
56
|
+
authorName: 'System',
|
|
57
|
+
value: null,
|
|
58
|
+
text,
|
|
59
|
+
createdAt,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(view.getByText(text)).toBeTruthy()
|
|
63
|
+
expect(view.queryByText('System', { exact: false })).toBeNull()
|
|
64
|
+
expect(
|
|
65
|
+
view.getByLabelText(
|
|
66
|
+
`Select Worship Team, Latest activity: ${text} at ${formatDatePreview(createdAt)}`
|
|
67
|
+
)
|
|
68
|
+
).toBeTruthy()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -168,6 +168,8 @@ function reactionEmoji(value: ConversationPreviewData['value']): string | null {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function formatConversationPreviewText(preview: ConversationPreviewData) {
|
|
171
|
+
if (preview.kind === 'system') return preview.text
|
|
172
|
+
|
|
171
173
|
if (preview.kind === 'reaction') {
|
|
172
174
|
const emoji = reactionEmoji(preview.value)
|
|
173
175
|
return `${preview.authorName}${emoji ? ` ${emoji}` : ' reacted'} to: "${preview.text}"`
|
|
@@ -179,6 +181,8 @@ function formatConversationPreviewText(preview: ConversationPreviewData) {
|
|
|
179
181
|
function formatConversationPreviewAccessibilityLabel(preview: ConversationPreviewData) {
|
|
180
182
|
const date = formatDatePreview(preview.createdAt)
|
|
181
183
|
|
|
184
|
+
if (preview.kind === 'system') return `Latest activity: ${preview.text} at ${date}`
|
|
185
|
+
|
|
182
186
|
if (preview.kind === 'reaction') {
|
|
183
187
|
return `${preview.authorName} reacted to your message sent ${date} and says ${preview.text}`
|
|
184
188
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
|
|
2
2
|
import { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'
|
|
3
|
+
import type { SystemTextFormat } from '../resources/message'
|
|
3
4
|
|
|
4
5
|
interface BaseMessageEventData extends Record<string, unknown> {
|
|
5
6
|
data: {
|
|
@@ -21,6 +22,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
|
|
|
21
22
|
message_type: string
|
|
22
23
|
system_event_person_ids: number[] | null
|
|
23
24
|
system_text_parts: { names: string[]; overflow_count: number; action: string } | null
|
|
25
|
+
system_text_format: SystemTextFormat | null
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -6,7 +6,7 @@ import { MemberAbilityResource } from './member_ability'
|
|
|
6
6
|
import { ReactionCountResource } from './reaction'
|
|
7
7
|
|
|
8
8
|
export interface ConversationPreviewData {
|
|
9
|
-
kind: 'reaction' | 'message'
|
|
9
|
+
kind: 'reaction' | 'message' | 'system'
|
|
10
10
|
authorName: string
|
|
11
11
|
value: ReactionCountResource['value'] | null
|
|
12
12
|
text: string
|
|
@@ -3,6 +3,8 @@ import { DenormalizedAttachmentResourceForCreate } from './denormalized_attachme
|
|
|
3
3
|
import type { PersonResource } from './person'
|
|
4
4
|
import type { ReactionCountResource } from './reaction'
|
|
5
5
|
|
|
6
|
+
export type SystemTextFormat = 'people' | 'actor'
|
|
7
|
+
|
|
6
8
|
export interface MessageResource {
|
|
7
9
|
type: 'Message'
|
|
8
10
|
id: string
|
|
@@ -22,6 +24,7 @@ export interface MessageResource {
|
|
|
22
24
|
messageType: string
|
|
23
25
|
personIdsForSystemEvent?: number[] | null
|
|
24
26
|
systemTextParts?: { names: string[]; overflowCount: number; action: string } | null
|
|
27
|
+
systemTextFormat?: SystemTextFormat | null
|
|
25
28
|
|
|
26
29
|
// Custom Local Properties we set for rendering
|
|
27
30
|
renderAuthor?: boolean
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { formatEventDateTime } from '../date'
|
|
2
|
+
|
|
3
|
+
describe('formatEventDateTime', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
jest.useFakeTimers({ now: new Date(2026, 5, 15, 12, 0) })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
jest.useRealTimers()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('leaves out the year for dates in the current year', () => {
|
|
13
|
+
expect(formatEventDateTime(new Date(2026, 1, 3, 14, 18))).toBe('Feb 3 at 2:18pm')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('includes the year for dates in other years', () => {
|
|
17
|
+
expect(formatEventDateTime(new Date(2025, 1, 3, 14, 18))).toBe('Feb 3, 2025 at 2:18pm')
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -206,6 +206,7 @@ describe('groupMessages — system messages', () => {
|
|
|
206
206
|
const messages = [
|
|
207
207
|
message('sys', {
|
|
208
208
|
messageType: 'user_joined',
|
|
209
|
+
systemTextFormat: 'people',
|
|
209
210
|
systemTextParts: { names: ['A'], overflowCount: 0, action: 'joined' },
|
|
210
211
|
personIdsForSystemEvent: [1],
|
|
211
212
|
}),
|
package/src/utils/date.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { date as formatDate } from '@planningcenter/datetime-fmt'
|
|
1
|
+
import { date as formatDate, time as formatTime } from '@planningcenter/datetime-fmt'
|
|
2
2
|
import dayjs from './dayjs'
|
|
3
3
|
|
|
4
4
|
export type DateProps = string | number | Date
|
|
@@ -61,3 +61,10 @@ export function relativeTime(date: DateProps): string {
|
|
|
61
61
|
return dayjs(date).format('h:mma')
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
export function formatEventDateTime(date: DateProps) {
|
|
66
|
+
const { isThisYear } = getRelativeDateStatus(date)
|
|
67
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
68
|
+
const day = formatDate(date, { style: 'abbreviated', year: !isThisYear, timeZone })
|
|
69
|
+
return `${day} at ${formatTime(date, { timeZone })}`
|
|
70
|
+
}
|