Skip to content

Commit 560b726

Browse files
committed
more love for the chat
1 parent f639da6 commit 560b726

File tree

5 files changed

+255
-62
lines changed

5 files changed

+255
-62
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@inherits MudComponentBase
2+
3+
<span class="@Classname"
4+
style="@Style"
5+
@attributes="@UserAttributes"
6+
@onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnButtonClick)">
7+
@ChildContent
8+
</span>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Web;
3+
using MudBlazor;
4+
using MudBlazor.Utilities;
5+
6+
namespace Jordnaer.Components;
7+
public partial class ScrollToBottom : IDisposable
8+
{
9+
private IScrollListener? _scrollListener;
10+
11+
protected string Classname =>
12+
new CssBuilder("mud-scroll-to-top")
13+
.AddClass("visible", Visible && string.IsNullOrWhiteSpace(VisibleCssClass))
14+
.AddClass("hidden", !Visible && string.IsNullOrWhiteSpace(HiddenCssClass))
15+
.AddClass(VisibleCssClass, Visible && !string.IsNullOrWhiteSpace(VisibleCssClass))
16+
.AddClass(HiddenCssClass, !Visible && !string.IsNullOrWhiteSpace(HiddenCssClass))
17+
.AddClass(Class)
18+
.Build();
19+
20+
[Inject]
21+
private IScrollListenerFactory ScrollListenerFactory { get; set; } = null!;
22+
23+
[Inject]
24+
private IScrollManager ScrollManager { get; set; } = null!;
25+
26+
[Parameter]
27+
[Category(CategoryTypes.ScrollToTop.Behavior)]
28+
public RenderFragment? ChildContent { get; set; }
29+
30+
/// <summary>
31+
/// The CSS selector to which the scroll event will be attached
32+
/// </summary>
33+
[Parameter]
34+
[Category(CategoryTypes.ScrollToTop.Behavior)]
35+
public string? Selector { get; set; }
36+
37+
/// <summary>
38+
/// If set to true, it starts Visible. If sets to false, it will become visible when the MinimumBottomOffset amount of scrolled pixels is reached
39+
/// </summary>
40+
[Parameter]
41+
[Category(CategoryTypes.ScrollToTop.Behavior)]
42+
public bool Visible { get; set; }
43+
44+
/// <summary>
45+
/// CSS class for the Visible state. Here, apply some transitions and animations that will happen when the component becomes visible
46+
/// </summary>
47+
[Parameter]
48+
[Category(CategoryTypes.ScrollToTop.Appearance)]
49+
public string? VisibleCssClass { get; set; }
50+
51+
/// <summary>
52+
/// CSS class for the Hidden state. Here, apply some transitions and animations that will happen when the component becomes invisible
53+
/// </summary>
54+
[Parameter]
55+
[Category(CategoryTypes.ScrollToTop.Appearance)]
56+
public string? HiddenCssClass { get; set; }
57+
58+
/// <summary>
59+
/// The distance in pixels scrolled from the bottom of the selected element from which
60+
/// the component becomes visible
61+
/// </summary>
62+
[Parameter]
63+
[Category(CategoryTypes.ScrollToTop.Behavior)]
64+
public int MinimumBottomOffset { get; set; } = 900;
65+
66+
/// <summary>
67+
/// Smooth or Auto
68+
/// </summary>
69+
[Parameter]
70+
[Category(CategoryTypes.ScrollToTop.Behavior)]
71+
public ScrollBehavior ScrollBehavior { get; set; } = ScrollBehavior.Smooth;
72+
73+
/// <summary>
74+
/// Called when scroll event is fired
75+
/// </summary>
76+
[Parameter]
77+
public EventCallback<ScrollEventArgs> OnScroll { get; set; }
78+
79+
[Parameter]
80+
public EventCallback<MouseEventArgs> OnClick { get; set; }
81+
82+
protected override void OnAfterRender(bool firstRender)
83+
{
84+
if (firstRender)
85+
{
86+
87+
var selector = !string.IsNullOrWhiteSpace(Selector)
88+
? Selector
89+
: null; // null is defaulted to document element in JS function
90+
91+
_scrollListener = ScrollListenerFactory.Create(selector);
92+
93+
//subscribe to event
94+
_scrollListener.OnScroll += ScrollListener_OnScroll;
95+
}
96+
}
97+
98+
/// <summary>
99+
/// event received when scroll in the selected element happens
100+
/// </summary>
101+
/// <param name="sender">ScrollListener instance</param>
102+
/// <param name="e">Information about the position of the scrolled element</param>
103+
private async void ScrollListener_OnScroll(object? sender, ScrollEventArgs e)
104+
{
105+
await OnScroll.InvokeAsync(e);
106+
107+
var distanceFromBottom = e.ScrollHeight - e.ScrollTop;
108+
109+
if (distanceFromBottom >= MinimumBottomOffset && Visible != true)
110+
{
111+
Visible = true;
112+
await InvokeAsync(StateHasChanged);
113+
}
114+
115+
if (distanceFromBottom < MinimumBottomOffset && Visible)
116+
{
117+
Visible = false;
118+
await InvokeAsync(StateHasChanged);
119+
}
120+
}
121+
122+
/// <summary>
123+
/// Scrolls to top when clicked and invokes OnClick
124+
/// </summary>
125+
private async Task OnButtonClick(MouseEventArgs args)
126+
{
127+
await ScrollManager.ScrollToBottomAsync(_scrollListener?.Selector, ScrollBehavior);
128+
await OnClick.InvokeAsync(args);
129+
}
130+
131+
/// <summary>
132+
/// Remove the event
133+
/// </summary>
134+
public void Dispose()
135+
{
136+
GC.SuppressFinalize(this);
137+
138+
if (_scrollListener is null)
139+
{
140+
return;
141+
}
142+
143+
_scrollListener.OnScroll -= ScrollListener_OnScroll;
144+
_scrollListener.Dispose();
145+
}
146+
}

src/web/Jordnaer/Extensions/DateTimeExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ public static class DateTimeExtensions
44
{
55
public static string DisplayExactTime(this DateTime date) =>
66
// Example of this format: 1/9 2017 kl. 07:45
7-
$"afsendt {date.ToLocalTime():d/M yyyy kl. HH:mm}";
7+
$"{date.ToLocalTime():d/M yyyy kl. HH:mm}";
88

99
public static string DisplayTimePassed(this DateTime date)
1010
{
@@ -42,7 +42,7 @@ public static string DisplayTimePassed(this DateTime date)
4242
return $"sendt for {Math.Floor(timeSpan.TotalDays)} dage siden";
4343
}
4444

45-
int weeks = (int)Math.Floor(timeSpan.TotalDays / 7);
45+
var weeks = (int)Math.Floor(timeSpan.TotalDays / 7);
4646

4747
if (weeks is 1)
4848
{

src/web/Jordnaer/Features/Chat/ChatMessageList.razor

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@using MudExtensions.Services
12
@inject CurrentUser CurrentUser
23

34
@inject IChatService ChatService
@@ -6,15 +7,19 @@
67
@inject IJSRuntime JsRuntime
78
@inject ChatSignalRClient ChatSignalRClient
89
@inject NavigationManager Navigation
10+
@inject IScrollManager ScrollManager
911

1012
@attribute [Authorize]
1113

14+
@if (ActiveChat is null && IsMobile)
15+
{
16+
return;
17+
}
18+
1219
<MudItem Class="@MudItemClass" Style="@MudItemStyle" xs="12" sm="12" md="8" lg="8" xl="8" xxl="8">
1320

1421
@switch (ActiveChat)
1522
{
16-
case null when IsMobile:
17-
return;
1823
case null when IsMobile is false:
1924
<MudStack Row Style="margin-left: -24px;">
2025
<MudIcon Icon="@Icons.Material.Filled.ArrowCircleLeft" Size="Size.Large" Color="Color.Info" />
@@ -35,47 +40,44 @@
3540
break;
3641
}
3742

38-
@*The elements below cannot be readonly/disabled, as it disabled the tooltips *@
43+
@*The elements below cannot be readonly/disabled, as it disables the tooltips *@
3944
<MudList Class="chat-message-list" Padding="false" Dense T="RenderFragment">
4045
<MudSpacer />
4146
<MudVirtualize Items="ActiveChat?.Messages" Context="message" OverscanCount="8" ItemSize="80">
47+
<MudListItem T="MudTooltip" Class="@(MudListItemClass(message))">
48+
49+
<MudTooltip Placement="@(IsMessageFromSelf(message) ? Placement.Left : Placement.Right)"
50+
Arrow
51+
Text="@(CreateTooltip(message))">
52+
53+
@if (IsMessageFromSelf(message))
54+
{
55+
<MudChip Class="chat-chip" Size="Size.Large" T="MudText">
56+
<MudText Typo="Typo.h6">@message.Text</MudText>
57+
</MudChip>
58+
}
59+
else
60+
{
61+
<MudChip Class="chat-chip" Size="Size.Large">
62+
<AvatarContent>
63+
@if (!NextMessageIsFromSameSender(message))
64+
{
65+
<MudLink Href="@GetRecipientsUserName(message)">
66+
<MudAvatar>
67+
<MudImage Src="@GetRecipientsProfilePictureUrl(message)" loading="lazy" Alt="Avatar"/>
68+
</MudAvatar>
69+
</MudLink>
70+
}
71+
</AvatarContent>
72+
<ChildContent>
73+
<MudText Typo="Typo.h6">@message.Text</MudText>
74+
</ChildContent>
75+
</MudChip>
76+
}
77+
78+
</MudTooltip>
79+
</MudListItem>
4280

43-
@if (IsMessageFromSelf(message))
44-
{
45-
<MudListItem Class="message-from-self" T="MudTooltip">
46-
<MudTooltip Placement="Placement.Top" ShowOnClick ShowOnHover="false" ShowOnFocus="false" Arrow Text="@message.SentUtc.DisplayExactTime()">
47-
<MudTooltip ShowOnHover Arrow Text="@message.SentUtc.DisplayTimePassed()">
48-
<MudChip Class="chat-chip" Size="Size.Large" T="MudText">
49-
<MudText Typo="Typo.h6">@message.Text</MudText>
50-
</MudChip>
51-
</MudTooltip>
52-
</MudTooltip>
53-
</MudListItem>
54-
}
55-
else
56-
{
57-
<MudListItem T="MudTooltip">
58-
<MudTooltip Placement="Placement.Top" ShowOnClick ShowOnHover="false" ShowOnFocus="false" Arrow Text="@message.SentUtc.DisplayExactTime()">
59-
<MudTooltip ShowOnHover Arrow Text="@message.SentUtc.DisplayTimePassed()">
60-
<MudChip Class="chat-chip" Size="Size.Large" T="MudLink">
61-
<AvatarContent>
62-
@if (!NextMessageIsFromSameSender(message))
63-
{
64-
<MudLink Href="@GetRecipientsUserName(message)">
65-
<MudAvatar>
66-
<MudImage Src="@GetRecipientsProfilePictureUrl(message)" loading="lazy" Alt="Avatar" />
67-
</MudAvatar>
68-
</MudLink>
69-
}
70-
</AvatarContent>
71-
<ChildContent>
72-
<MudText Typo="Typo.h6">@message.Text</MudText>
73-
</ChildContent>
74-
</MudChip>
75-
</MudTooltip>
76-
</MudTooltip>
77-
</MudListItem>
78-
}
7981
</MudVirtualize>
8082

8183
@if (LastMessageWasSentSuccessfullyByCurrentUser)
@@ -102,11 +104,18 @@
102104
Lines="2"
103105
IconSize="Size.Large"
104106
Style="@($"background: {JordnaerTheme.CustomTheme.PaletteLight.BackgroundGray}")" />
107+
105108
</MudList>
109+
110+
<ScrollToBottom Selector="@ChatMessageWindowClass">
111+
<MudFab Color="Color.Primary" StartIcon="@Icons.Material.Filled.ArrowCircleDown" />
112+
</ScrollToBottom>
106113
</MudItem>
107114

108115
@code
109116
{
117+
private const string ChatMessageWindowClass = ".chat-message-window";
118+
110119
[Parameter, EditorRequired]
111120
public required ChatDto? ActiveChat { get; set; }
112121

@@ -132,15 +141,17 @@
132141
? "padding: 0"
133142
: "";
134143

144+
private string MudListItemClass(ChatMessageDto message) => IsMessageFromSelf(message)
145+
? "message-from-self"
146+
: "";
147+
135148
private string GetRecipientsProfilePictureUrl(ChatMessageDto message)
136-
=> ActiveChat?
137-
.Recipients
138-
.FirstOrDefault(recipient => recipient.Id == message.SenderId)?.ProfilePictureUrl
149+
=> ActiveChat?.Recipients
150+
.FirstOrDefault(recipient => recipient.Id == message.SenderId)?.ProfilePictureUrl
139151
?? ProfileConstants.Default_Profile_Picture;
140152

141153
private string? GetRecipientsUserName(ChatMessageDto message)
142-
=> ActiveChat?
143-
.Recipients
154+
=> ActiveChat?.Recipients
144155
.FirstOrDefault(recipient => recipient.Id == message.SenderId)?.UserName;
145156

146157
private bool NextMessageIsFromSameSender(ChatMessageDto message)
@@ -164,7 +175,7 @@
164175

165176
protected override async Task OnAfterRenderAsync(bool firstRender)
166177
{
167-
await JsRuntime.ScrollToBottomOfElement(".chat-message-window");
178+
await ScrollManager.ScrollToBottomAsync(ChatMessageWindowClass);
168179
}
169180

170181
private void MarkMessageIfSuccessfullySentByCurrentUser()
@@ -222,16 +233,17 @@
222233
}
223234

224235
await _messageInput.FocusAsync();
225-
await JsRuntime.ScrollToBottomOfElement(".chat-message-window");
236+
await ScrollManager.ScrollToBottomAsync(ChatMessageWindowClass);
226237
}
227238

228-
// TODO: Add MudScrollToTop that shows when user is scrolled up in the chat window
229-
230239
private async Task SendMessageOnEnter(KeyboardEventArgs keyboardEventArgs)
231240
{
232241
if (keyboardEventArgs is { Key: "Enter", ShiftKey: false })
233242
{
234243
await SendMessage();
235244
}
236245
}
246+
247+
private string CreateTooltip(ChatMessageDto message)
248+
=> $"{message.SentUtc.DisplayTimePassed()} ({message.SentUtc.DisplayExactTime()})";
237249
}

0 commit comments

Comments
 (0)