Skip to content

Commit cd5ad40

Browse files
authored
Merge pull request #443 from GetStream/date-separator
feat: Add date separator component
2 parents 67583c0 + 2c7b112 commit cd5ad40

18 files changed

+357
-44
lines changed

docusaurus/docs/Angular/components/MessageComponent.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The `Message` component displays a message with additional information such as s
2020

2121
The message list uses the `Message` component to display messages, if you want to replace that with your own custom component, you can do that by providing it to the [`CustomTemplatesService`](../services/CustomTemplatesService.mdx/#messagetemplate). It's also possible to just override parts of the message component.
2222

23+
The `Message` component uses the [`DateParserService`](../services/DateParserService.mdx) to create user-friendly dates, you can override the date-parsing methods with your own functions.
24+
2325
:::note
2426

2527
If you want to build your own `Message` component, you might find the following building blocks useful:
@@ -40,10 +42,6 @@ The `Message` component uses the [`MessageReactions`](./MessageReactionsComponen
4042

4143
The `Message` component uses the [`AttachmentList`](./AttachmentListComponent.mdx) component to display the attachments of the message.
4244

43-
### User friendly dates
44-
45-
The `Message` component uses the [`parseDate`](https://github.com/GetStream/stream-chat-angular/tree/master/projects/stream-chat-angular/src/lib/message/parse-date.ts) utility function to create user-friendly dates (for example _Yesterday at 8:15 AM_).
46-
4745
### Read by
4846

4947
The `Message` component uses the [`listUsers`](https://github.com/GetStream/stream-chat-angular/tree/master/projects/stream-chat-angular/src/lib/list-users.ts) utility function to display the list of people who have read a particular message in a user friendly way (for example _Bob, Sophie, Jack, Rose, John and 1 more_).

docusaurus/docs/Angular/components/MessageListComponent.mdx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ The `MessageList` displays messages using pagination, new messages are loaded wh
77

88
## Customization
99

10-
See [our customization guide](../concepts/customization.mdx) on how to provide your own message list component.
10+
### Custom message component
11+
12+
If you want to customize how messages are displayed in the list, head over to the [`Message` component's page](../../components/MessageComponent/#customization).
13+
14+
### Date separator
15+
16+
- The component provides the [`displayDateSeparator`](#displaydateseparator) and [`dateSeparatorTextPos`](#dateseparatortextpos) inputs for configuring the date separator's behavior.
17+
- You can customize the displayed date format by providing your own [date parser](../../services/DateParserService/#customdateparser).
18+
- If you want to provide your own date separator UI, provide that to the [`CustomTemplatesService`](../../services/CustomTemplatesService/#dateseparatortemplate).
19+
20+
### Custom message list
21+
22+
The message list contains a lot of low-level logic related to scrolling, we don't advise to create your own message list component, however it's possible: see [our customization guide](../concepts/customization.mdx) on how to provide your own message list component.
1123

1224
:::note
1325
If you want to create your own message list, you can use the [`ChannelService`](../services/ChannelService.mdx) to receive the necessary messages and interact with the Stream API.

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
"@ngx-translate/core": "^13.0.0",
118118
"@ngx-translate/http-loader": "^6.0.0",
119119
"@popperjs/core": "^2.11.5",
120-
"@stream-io/stream-chat-css": "3.9.0",
120+
"@stream-io/stream-chat-css": "3.10.1",
121121
"@stream-io/transliterate": "^1.5.2",
122122
"angular-mentions": "^1.4.0",
123123
"dayjs": "^1.10.7",

projects/customizations-example/src/app/app.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,7 @@
255255
/>
256256
<button (click)="addRandomImage(attachmentService)">Random image</button>
257257
</ng-template>
258+
259+
<ng-template #dateSeparator let-date="date" let-parsedDate="parsedDate">
260+
{{ date }} - {{ parsedDate }}
261+
</ng-template>

projects/customizations-example/src/app/app.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
NotificationContext,
3636
ThreadHeaderContext,
3737
CustomAttachmentUploadContext,
38+
DateSeparatorContext,
3839
} from 'stream-chat-angular';
3940
import { environment } from '../environments/environment';
4041

@@ -88,6 +89,8 @@ export class AppComponent implements AfterViewInit {
8889
private chstomChannelInfoTemplate!: TemplateRef<ChannelHeaderInfoContext>;
8990
@ViewChild('customAttachmentUpload')
9091
private customAttachmentUploadTemplate!: TemplateRef<CustomAttachmentUploadContext>;
92+
@ViewChild('dateSeparator')
93+
private dateSeparatorTemplate!: TemplateRef<DateSeparatorContext>;
9194

9295
constructor(
9396
private chatService: ChatClientService,
@@ -164,6 +167,9 @@ export class AppComponent implements AfterViewInit {
164167
this.customTemplatesService.customAttachmentUploadTemplate$.next(
165168
this.customAttachmentUploadTemplate
166169
);
170+
this.customTemplatesService.dateSeparatorTemplate$.next(
171+
this.dateSeparatorTemplate
172+
);
167173
}
168174

169175
inviteClicked(channel: Channel) {

projects/stream-chat-angular/src/lib/custom-templates.service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ChannelPreviewContext,
1111
CommandAutocompleteListItemContext,
1212
CustomAttachmentUploadContext,
13+
DateSeparatorContext,
1314
DeliveredStatusContext,
1415
EmojiPickerContext,
1516
IconContext,
@@ -279,6 +280,12 @@ export class CustomTemplatesService {
279280
systemMessageTemplate$ = new BehaviorSubject<
280281
TemplateRef<SystemMessageContext> | undefined
281282
>(undefined);
283+
/**
284+
* The template used to display the date separator inside the [message list](../components/MessageListComponent.mdx)
285+
*/
286+
dateSeparatorTemplate$ = new BehaviorSubject<
287+
TemplateRef<DateSeparatorContext> | undefined
288+
>(undefined);
282289

283290
constructor() {}
284291
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { DateParserService } from './date-parser.service';
4+
5+
describe('DateParserService', () => {
6+
let service: DateParserService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(DateParserService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
17+
it('should parse date', () => {
18+
const date = new Date(2023, 7, 2);
19+
20+
expect(service.parseDate(date)).toEqual('02/08/2023');
21+
});
22+
23+
it('should parse date-time', () => {
24+
const date = new Date();
25+
26+
expect(service.parseDateTime(date)).toContain('Today at');
27+
});
28+
29+
it('should call custom date parser', () => {
30+
const date = new Date(2023, 7, 2);
31+
const spy = jasmine.createSpy();
32+
service.customDateParser = spy;
33+
34+
service.parseDate(date);
35+
36+
expect(spy).toHaveBeenCalledWith(date);
37+
});
38+
39+
it('should call custom date-time', () => {
40+
const date = new Date(2023, 7, 2);
41+
const spy = jasmine.createSpy();
42+
service.customDateTimeParser = spy;
43+
44+
service.parseDateTime(date);
45+
46+
expect(spy).toHaveBeenCalledWith(date);
47+
});
48+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Injectable } from '@angular/core';
2+
import { parseDate } from './parse-date';
3+
4+
/**
5+
* The `DateParserService` parses dates into user-friendly string representations.
6+
*/
7+
@Injectable({
8+
providedIn: 'root',
9+
})
10+
export class DateParserService {
11+
/**
12+
* Custom parser to override `parseDate`
13+
*/
14+
customDateParser?: (date: Date) => string;
15+
/**
16+
* Custom parser to override `parseDateTime`
17+
*/
18+
customDateTimeParser?: (date: Date) => string;
19+
20+
constructor() {}
21+
22+
/**
23+
* Return a user-friendly string representation of the date (year, month and date)
24+
* @param date
25+
* @returns
26+
*/
27+
parseDate(date: Date) {
28+
if (this.customDateParser) {
29+
return this.customDateParser(date);
30+
}
31+
return parseDate(date, 'date');
32+
}
33+
34+
/**
35+
* Return a user-friendly string representation of the date and time
36+
* @param date
37+
* @returns
38+
*/
39+
parseDateTime(date: Date) {
40+
if (this.customDateTimeParser) {
41+
return this.customDateTimeParser(date);
42+
}
43+
return parseDate(date, 'date-time');
44+
}
45+
}

projects/stream-chat-angular/src/lib/message-list/message-list.component.html

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,52 @@
3232
data-testid="top-loading-indicator"
3333
*ngIf="isLoading && direction === 'bottom-to-top'"
3434
></stream-loading-indicator>
35-
<li
36-
tabindex="0"
37-
data-testclass="message"
38-
*ngFor="
39-
let message of messages$ | async;
40-
let i = index;
41-
trackBy: trackByMessageId
42-
"
43-
class="str-chat__li str-chat__li--{{ groupStyles[i] }}"
44-
id="{{ message.id }}"
45-
>
35+
<ng-container *ngIf="messages$ | async as messages">
4636
<ng-container
47-
*ngTemplateOutlet="
48-
messageTemplateContainer;
49-
context: { message: message, index: i }
37+
*ngFor="
38+
let message of messages;
39+
let i = index;
40+
let isFirst = first;
41+
trackBy: trackByMessageId
5042
"
51-
></ng-container>
52-
</li>
43+
>
44+
<ng-container *ngIf="isFirst">
45+
<ng-container
46+
*ngTemplateOutlet="
47+
dateSeparator;
48+
context: {
49+
date: message.created_at,
50+
parsedDate: parseDate(message.created_at)
51+
}
52+
"
53+
></ng-container>
54+
</ng-container>
55+
<li
56+
tabindex="0"
57+
data-testclass="message"
58+
class="str-chat__li str-chat__li--{{ groupStyles[i] }}"
59+
id="{{ message.id }}"
60+
>
61+
<ng-container
62+
*ngTemplateOutlet="
63+
messageTemplateContainer;
64+
context: { message: message, index: i }
65+
"
66+
></ng-container>
67+
</li>
68+
<ng-container *ngIf="areOnSeparateDates(message, messages[i + 1])">
69+
<ng-container
70+
*ngTemplateOutlet="
71+
dateSeparator;
72+
context: {
73+
date: messages[i + 1].created_at,
74+
parsedDate: parseDate(messages[i + 1].created_at)
75+
}
76+
"
77+
></ng-container>
78+
</ng-container>
79+
</ng-container>
80+
</ng-container>
5381
<stream-loading-indicator
5482
data-testid="bottom-loading-indicator"
5583
*ngIf="isLoading && direction === 'top-to-bottom'"
@@ -158,3 +186,38 @@
158186
"
159187
></ng-container>
160188
</ng-template>
189+
190+
<ng-template #dateSeparator let-date="date" let-parsedDate="parsedDate">
191+
<ng-container *ngIf="displayDateSeparator">
192+
<ng-container
193+
*ngTemplateOutlet="
194+
customDateSeparatorTemplate || defaultDateSeparator;
195+
context: { date: date, parsedDate: parsedDate }
196+
"
197+
></ng-container>
198+
</ng-container>
199+
200+
<ng-template
201+
#defaultDateSeparator
202+
let-date="date"
203+
let-parsedDate="parsedDate"
204+
>
205+
<div data-testid="date-separator" class="str-chat__date-separator">
206+
<hr
207+
*ngIf="
208+
dateSeparatorTextPos === 'right' || dateSeparatorTextPos === 'center'
209+
"
210+
class="str-chat__date-separator-line"
211+
/>
212+
<div class="str-chat__date-separator-date">
213+
{{ parsedDate }}
214+
</div>
215+
<hr
216+
*ngIf="
217+
dateSeparatorTextPos === 'left' || dateSeparatorTextPos === 'center'
218+
"
219+
class="str-chat__date-separator-line"
220+
/>
221+
</div>
222+
</ng-template>
223+
</ng-template>

0 commit comments

Comments
 (0)