1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 06:52:56 +08:00

Implement basic layout and behaviour of new chat overlay

Provides initial implementation of new chat overlay in component
`ChatOverlayV2`. Contains only the basic functionality required for
a functioning chat overlay according to the new design with the intent
of added the rest of the functionality in subsequent PRs.

Backports existing tests for the current chat overlay except for ones
testing keyboard shortcuts (since they haven't been added) and tab
closing behaviour (since no tabs).
This commit is contained in:
Jai Sharma 2022-04-29 21:33:32 +01:00
parent 9fb10ca852
commit de393f735f
5 changed files with 743 additions and 18 deletions

View File

@ -0,0 +1,365 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
using osu.Game.Overlays.Chat.Listing;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
{
private ChatOverlayV2 chatOverlay;
private ChannelManager channelManager;
private readonly APIUser testUser;
private readonly Channel testPMChannel;
private readonly Channel[] testChannels;
private Channel testChannel1 => testChannels[0];
private Channel testChannel2 => testChannels[1];
public TestSceneChatOverlayV2()
{
testUser = new APIUser { Username = "test user", Id = 5071479 };
testPMChannel = new Channel(testUser);
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
}
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ChannelManager), channelManager = new ChannelManager()),
},
Children = new Drawable[]
{
channelManager,
chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both },
},
};
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Setup request handler", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetUpdatesRequest getUpdates:
getUpdates.TriggerFailure(new WebException());
return true;
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
case LeaveChannelRequest leaveChannel:
leaveChannel.TriggerSuccess();
return true;
case GetMessagesRequest getMessages:
getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
return true;
case GetUserRequest getUser:
if (getUser.Lookup == testUser.Username)
getUser.TriggerSuccess(testUser);
else
getUser.TriggerFailure(new WebException());
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
{
Content = postMessage.Message.Content,
ChannelId = postMessage.Message.ChannelId,
Sender = postMessage.Message.Sender,
Timestamp = new DateTimeOffset(DateTime.Now),
});
return true;
default:
Logger.Log($"Unhandled Request Type: {req.GetType()}");
return false;
}
};
});
AddStep("Add test channels", () =>
{
(channelManager.AvailableChannels as BindableList<Channel>)?.AddRange(testChannels);
});
}
[Test]
public void TestShowHide()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
AddStep("Hide overlay", () => chatOverlay.Hide());
AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestChannelSelection()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden);
AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden);
AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1);
AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1);
}
[Test]
public void TestSearchInListing()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible);
AddStep("Search for 'number 2'", () => chatOverlay.ChildrenOfType<ChatTextBox>().Single().Text = "number 2");
AddUntilStep("Only channel 2 visibile", () =>
{
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent);
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
});
}
[Test]
public void TestChannelCloseButton()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join PM and public channels", () =>
{
channelManager.JoinChannel(testChannel1);
channelManager.JoinChannel(testPMChannel);
});
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddStep("Click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
}
[Test]
public void TestChatCommand()
{
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel);
AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel);
AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
// Make sure no unnecessary requests are made when the PM channel is already open.
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel);
AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel);
}
[Test]
public void TestMultiplayerChannelIsNotShown()
{
Channel multiplayerChannel = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
Type = ChannelType.Multiplayer,
}));
AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent)
.Select(item => item.Channel)
.Contains(multiplayerChannel));
}
[Test]
public void TestHighlightOnCurrentChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
[Test]
public void TestHighlightOnAnotherChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
testChannel2.AddNewMessages(message = new Message
{
ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
}
[Test]
public void TestHighlightOnLeftChannel()
{
Message message = null;
AddStep("Show overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
AddStep("Send message in channel 2", () =>
{
testChannel2.AddNewMessages(message = new Message
{
ChannelId = testChannel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2);
}
[Test]
public void TestHighlightWhileChatNeverOpen()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
[Test]
public void TestHighlightWithNullChannel()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddStep("Send message in channel 1", () =>
{
testChannel1.AddNewMessages(message = new Message
{
ChannelId = testChannel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = testUser,
});
});
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
}
private Visibility listingVisibility =>
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value;
private Visibility loadingVisibility =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value;
private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<Container<DrawableChannel>>().Single().Child;
private ChannelListItem getChannelListItem(Channel channel) =>
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
private void clickDrawable(Drawable d)
{
InputManager.MoveMouseTo(d);
InputManager.Click(MouseButton.Left);
}
private List<Message> createChannelMessages(Channel channel)
{
var message = new Message
{
ChannelId = channel.Id,
Content = $"Hello, this is a message in {channel.Name}",
Sender = testUser,
Timestamp = new DateTimeOffset(DateTime.Now),
};
return new List<Message> { message };
}
private Channel createPublicChannel(int id) => new Channel
{
Id = id,
Name = $"#channel-{id}",
Topic = $"We talk about the number {id} here",
Type = ChannelType.Public,
};
}
}

View File

@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList
public event Action<Channel>? OnRequestSelect;
public event Action<Channel>? OnRequestLeave;
public readonly Channel Channel;
public readonly BindableInt Mentions = new BindableInt();
public readonly BindableBool Unread = new BindableBool();
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Channel channel;
private Box hoverBox = null!;
private Box selectBox = null!;
private OsuSpriteText text = null!;
@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
public ChannelListItem(Channel channel)
{
this.channel = channel;
Channel = channel;
}
[BackgroundDependencyLoader]
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Name,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 3 },
Action = () => OnRequestLeave?.Invoke(channel),
Action = () => OnRequestLeave?.Invoke(Channel),
}
}
},
@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
},
};
Action = () => OnRequestSelect?.Invoke(channel);
Action = () => OnRequestSelect?.Invoke(Channel);
}
protected override void LoadComplete()
@ -151,10 +151,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
private Drawable createIcon()
{
if (channel.Type != ChannelType.PM)
if (Channel.Type != ChannelType.PM)
return Drawable.Empty();
return new UpdateableAvatar(channel.Users.First(), isInteractive: false)
return new UpdateableAvatar(Channel.Users.First(), isInteractive: false)
{
Size = new Vector2(20),
Margin = new MarginPadding { Right = 5 },
@ -167,7 +167,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
private void updateSelectState()
{
if (selectedChannel.Value == channel && !SelectorActive.Value)
if (selectedChannel.Value == Channel && !SelectorActive.Value)
selectBox.FadeIn(300, Easing.OutQuint);
else
selectBox.FadeOut(200, Easing.OutQuint);

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Chat
{
public class ChatOverlayTopBar : Container
{
// IsHovered is used by overlay
public override bool HandlePositionalInput => true;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, TextureStore textures)
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 50),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = textures.Get("Icons/Hexacons/messaging"),
Size = new Vector2(18),
},
// Placeholder text
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = "osu!chat",
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2f },
},
},
},
},
};
}
}
}

View File

@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing
public event Action<Channel>? OnRequestJoin;
public event Action<Channel>? OnRequestLeave;
public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
public readonly Channel Channel;
private readonly Channel channel;
public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
private Box hoverBox = null!;
private SpriteIcon checkbox = null!;
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing
public ChannelListingItem(Channel channel)
{
this.channel = channel;
Channel = channel;
}
[BackgroundDependencyLoader]
@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Name,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2 },
},
@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Topic,
Text = Channel.Topic,
Font = OsuFont.Torus.With(size: text_size),
Margin = new MarginPadding { Bottom = 2 },
},
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing
{
base.LoadComplete();
channelJoined = channel.Joined.GetBoundCopy();
channelJoined = Channel.Joined.GetBoundCopy();
channelJoined.BindValueChanged(change =>
{
const double duration = 500;
@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing
}
}, true);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
}
protected override bool OnHover(HoverEvent e)

View File

@ -0,0 +1,293 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
using osu.Game.Overlays.Chat.ChannelList;
using osu.Game.Overlays.Chat.Listing;
namespace osu.Game.Overlays
{
public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent
{
public string IconTexture => "Icons/Hexacons/messaging";
public LocalisableString Title => ChatStrings.HeaderTitle;
public LocalisableString Description => ChatStrings.HeaderDescription;
private ChatOverlayTopBar topBar = null!;
private ChannelList channelList = null!;
private LoadingLayer loading = null!;
private ChannelListing channelListing = null!;
private ChatTextBar textBar = null!;
private Container<DrawableChannel> currentChannelContainer = null!;
private Bindable<float>? chatHeight;
private bool isDraggingTopBar;
private float dragStartChatHeight;
private const int transition_length = 500;
private const float default_chat_height = 0.4f;
private const float top_bar_height = 40;
private const float side_bar_width = 190;
private const float chat_bar_height = 60;
private readonly BindableBool selectorActive = new BindableBool();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private ChannelManager channelManager { get; set; } = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
[BackgroundDependencyLoader]
private void load()
{
// Width = 0.85f; // Matches OnlineOverlay
Height = default_chat_height;
RelativeSizeAxes = Axes.Both;
RelativePositionAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Masking = true;
CornerRadius = 7f;
Margin = new MarginPadding { Bottom = -10 };
Padding = new MarginPadding { Bottom = 10 };
Children = new Drawable[]
{
topBar = new ChatOverlayTopBar
{
RelativeSizeAxes = Axes.X,
Height = top_bar_height,
},
channelList = new ChannelList
{
RelativeSizeAxes = Axes.Y,
Width = side_bar_width,
Padding = new MarginPadding { Top = top_bar_height },
SelectorActive = { BindTarget = selectorActive },
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Padding = new MarginPadding
{
Top = top_bar_height,
Left = side_bar_width,
Bottom = chat_bar_height,
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
},
loading = new LoadingLayer(true),
channelListing = new ChannelListing
{
RelativeSizeAxes = Axes.Both,
},
},
},
textBar = new ChatTextBar
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Padding = new MarginPadding { Left = side_bar_width },
ShowSearch = { BindTarget = selectorActive },
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
loading.Show();
chatHeight = config.GetBindable<float>(OsuSetting.ChatDisplayHeight).GetBoundCopy();
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
currentChannel.BindTo(channelManager.CurrentChannel);
channelManager.CurrentChannel.BindValueChanged(currentChannelChanged, true);
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true);
channelList.OnRequestSelect += channel =>
{
// Manually selecting a channel should dismiss the selector
selectorActive.Value = false;
channelManager.CurrentChannel.Value = channel;
};
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
textBar.OnChatMessageCommitted += handleChatMessage;
selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true);
}
/// <summary>
/// Highlights a certain message in the specified channel.
/// </summary>
/// <param name="message">The message to highlight.</param>
/// <param name="channel">The channel containing the message.</param>
public void HighlightMessage(Message message, Channel channel)
{
Debug.Assert(channel.Id == message.ChannelId);
if (currentChannel.Value?.Id != channel.Id)
{
if (!channel.Joined.Value)
channel = channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
}
selectorActive.Value = false;
channel.HighlightedMessage.Value = message;
Show();
}
protected override bool OnDragStart(DragStartEvent e)
{
isDraggingTopBar = topBar.IsHovered;
if (!isDraggingTopBar)
return base.OnDragStart(e);
dragStartChatHeight = chatHeight!.Value;
return true;
}
protected override void OnDrag(DragEvent e)
{
if (!isDraggingTopBar)
return;
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
chatHeight!.Value = targetChatHeight;
}
protected override void OnDragEnd(DragEndEvent e)
{
isDraggingTopBar = false;
base.OnDragEnd(e);
}
protected override void PopIn()
{
this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint);
base.PopIn();
}
protected override void PopOut()
{
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
base.PopOut();
}
private void currentChannelChanged(ValueChangedEvent<Channel> e)
{
Channel? newChannel = e.NewValue;
loading.Show();
// Channel is null when leaving the currently selected channel
if (newChannel == null)
{
// Find another channel to switch to
newChannel = channelManager.JoinedChannels.FirstOrDefault(chan => chan != e.OldValue);
if (newChannel == null)
selectorActive.Value = true;
else
currentChannel.Value = newChannel;
return;
}
LoadComponentAsync(new DrawableChannel(newChannel), loaded =>
{
currentChannelContainer.Clear();
currentChannelContainer.Add(loaded);
loading.Hide();
});
}
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
IEnumerable<Channel> joinedChannels = filterChannels(args.NewItems);
foreach (var channel in joinedChannels)
channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
foreach (var channel in leftChannels)
channelList.RemoveChannel(channel);
break;
}
}
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
=> channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
private IEnumerable<Channel> filterChannels(IList channels)
=> channels.Cast<Channel>().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM);
private void handleChatMessage(string message)
{
if (string.IsNullOrWhiteSpace(message))
return;
if (message[0] == '/')
channelManager.PostCommand(message.Substring(1));
else
channelManager.PostMessage(message);
}
}
}