From afaf7a214ceb3704a6495c298f318348b3174898 Mon Sep 17 00:00:00 2001 From: Lyndon Jetten Date: Tue, 7 Sep 2021 19:32:10 +0200 Subject: [PATCH] - Links in Nachrichten klickbar - Profilbilder werden in Gruppenchats (mehr als 2 Teilnehmer) angezeigt - Profilbilder werden im Cache gespeichert - Verbesserungen am Code --- ChatController/ChatController.csproj | 2 + ChatController/ChatKlassen/ChatMessage.cs | 220 +++++------------- ChatController/ChatKlassen/Contact.cs | 11 +- ChatController/ChatMainControl.xaml | 113 +++++---- ChatController/ChatMainControl.xaml.cs | 82 ++++--- .../UserProfilePictureVisibilityConverter.cs | 30 +++ ChatController/Core/BindableTextBlock.cs | 31 +++ ChatController/Core/OwnChatCache.cs | 37 +++ ChatController/HauptKlassen/Chat.cs | 159 ++++++------- ChatController/LoginControl.xaml | 31 ++- ChatController/LoginControl.xaml.cs | 16 +- ChatController/LoginKlassen/UserDaten.cs | 20 ++ ChatController/Utilities/Utils.cs | 155 +++++++++--- PrototypChat/.vs/PrototypChat/v16/.suo | Bin 229888 -> 288256 bytes 14 files changed, 535 insertions(+), 372 deletions(-) create mode 100644 ChatController/Converter/UserProfilePictureVisibilityConverter.cs create mode 100644 ChatController/Core/BindableTextBlock.cs diff --git a/ChatController/ChatController.csproj b/ChatController/ChatController.csproj index 2e5e772..eae6e56 100644 --- a/ChatController/ChatController.csproj +++ b/ChatController/ChatController.csproj @@ -72,6 +72,8 @@ + + diff --git a/ChatController/ChatKlassen/ChatMessage.cs b/ChatController/ChatKlassen/ChatMessage.cs index a2c9432..7fc0259 100644 --- a/ChatController/ChatKlassen/ChatMessage.cs +++ b/ChatController/ChatKlassen/ChatMessage.cs @@ -1,12 +1,14 @@ using System; +using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics; -using System.Text.RegularExpressions; -using System.Windows; +using System.Windows.Documents; using System.Windows.Media; using ChatController.Core; +using ChatController.HauptKlassen; using ChatController.Utilities; using static System.Windows.HorizontalAlignment; +using Application = System.Windows.Application; +using HorizontalAlignment = System.Windows.HorizontalAlignment; namespace ChatController.ChatKlassen { @@ -22,108 +24,82 @@ namespace ChatController.ChatKlassen public bool IsSeparator { get; set; } - public long FileSize { get; set; } + public long SenderId { get; } - // Separator - public ChatMessage(DateTime sendTime) + private ObservableCollection _MessageInlines; + + public ObservableCollection MessageInlines { - Id = Guid.NewGuid(); - IsSeparator = true; - SendTime = sendTime; - - MessageType = ChatMessageType.Separator; + get => _MessageInlines; + set + { + if(false == (_MessageInlines?.Equals(value) ?? false)) + { + _MessageInlines = value; + OnPropertyChanged(nameof(MessageInlines)); + } + } } - //Chat-Ansicht - public ChatMessage(string username, string message, DateTime sendtime, bool isme, long groupId, long fileSize, long messageId) + public ChatMessage(long groupId, long messageId, string userName, string messageText, DateTime sendTime, bool isOwnMessage, long fileSize, string filePath, + string originalImage, string imageName, string thumbnailPath, ImageSource logo, ChatMessageType chatMessageType, long senderId) { - MessageId = messageId; - Id = Guid.NewGuid(); - Username = username; - UserMessage = message; - SendTime = sendtime; - IsMyMessage = isme; + SenderId = senderId; + + MessageType = chatMessageType; + + MessageId = messageId; GroupId = groupId; + Username = userName; - MessageType = IsHyperlink(message) ? ChatMessageType.Hyperlink : ChatMessageType.Message; - } - - // Image-Ansicht - public ChatMessage(string username, string message, DateTime sendtime, bool isme, string filePath, string originalImage, string imageName, long groupId, long fileSize, long messageId) - { - PicturePlaceholderHeight = Utils.GetHeightFromThumbnailUri(filePath); - PictureSource = Utils.GetPicturePlaceholder(); + UserMessage = messageText; + if(!string.IsNullOrEmpty(messageText)) + { + Application.Current.Dispatcher.Invoke(() => + { + MessageInlines = Utils.ConvertLinksToHyperlinks(messageText); + }); + } - MessageId = messageId; - - Id = Guid.NewGuid(); - Username = username; - UserMessage = message; - SendTime = sendtime; - - IsMyMessage = isme; + SendTime = sendTime; + IsMyMessage = isOwnMessage; OriginalImage = originalImage; OriginalImageName = imageName; - - GroupId = groupId; - MessageType = ChatMessageType.Image; - - Utils.DownloadImageAsync(filePath, $"group-{groupId}", CacheCategory.Thumbnail, imageSource => + if(MessageId == 0 && MessageType == ChatMessageType.Image && logo != null) { - PictureSource = imageSource; - }); - } - - //Dokumenten-Ansicht - public ChatMessage(string username, string message, DateTime sendtime, string thumbnailPath, bool isme, string filePath, long groupId, long fileSize, long messageId) - { - ThumbnailPlaceholderHeight = Utils.GetHeightFromThumbnailUri(thumbnailPath); - Thumbnail = Utils.GetPicturePlaceholder(); - - MessageId = messageId; - - Id = Guid.NewGuid(); - Username = username; - UserMessage = message; - SendTime = sendtime; - IsMyMessage = isme; + PictureSource = logo; + PicturePlaceholderHeight = (int) logo.Height; + } FilePath = filePath; - - Utils.DownloadImageAsync(thumbnailPath, $"group-{groupId}", CacheCategory.Thumbnail, imageSource => + + if(!string.IsNullOrWhiteSpace(thumbnailPath)) { - Thumbnail = imageSource; - }); + ThumbnailPlaceholderHeight = Utils.GetHeightFromThumbnailUri(thumbnailPath); + Thumbnail = Utils.GetPicturePlaceholder(); - GroupId = groupId; + Utils.DownloadImageAsync(thumbnailPath, $"group-{groupId}", CacheCategory.Thumbnail, imageSource => + { + Thumbnail = imageSource; + }); + } - MessageType = ChatMessageType.Document; - } + if(MessageType == ChatMessageType.Image && !string.IsNullOrWhiteSpace(filePath)) + { + PicturePlaceholderHeight = Utils.GetHeightFromThumbnailUri(filePath); + PictureSource = Utils.GetPicturePlaceholder(); - // Mit Logo - public ChatMessage(string username, string message, DateTime sendtime, bool isme, ImageSource logo, string originalImage, string imageName, long groupId, long fileSize, long messageId) - { - MessageId = messageId; + Utils.DownloadImageAsync(filePath, $"group-{groupId}", CacheCategory.Thumbnail, imageSource => + { + PictureSource = imageSource; + }); + } - Id = Guid.NewGuid(); - Username = username; - UserMessage = message; - SendTime = sendtime; - - IsMyMessage = isme; - - OriginalImage = originalImage; - OriginalImageName = imageName; - - GroupId = groupId; - - MessageType = ChatMessageType.Image; - - PictureSource = logo; + IsSeparator = MessageType == ChatMessageType.Separator; } public ChatMessageType MessageType { get; } @@ -157,47 +133,6 @@ namespace ChatController.ChatKlassen } } - private static readonly Regex _UrlRegex = new Regex(@"(?#Protocol)^(http(?:s?)\:(\/\/|\\\\)|(w){3}(2|3)?\.{1})(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?"); - - public static bool IsHyperlink(string word) - { - if(word == null) - { - return false; - } - - try - { - if (word.IndexOfAny(@":.\/".ToCharArray()) != -1) - { - if (_UrlRegex.IsMatch(word)) - { - var uri = new Uri(word, UriKind.RelativeOrAbsolute); - - if (!uri.IsAbsoluteUri) - { - uri = new Uri(@"http://" + word, UriKind.Absolute); - } - - if (uri.IsAbsoluteUri) - { - return true; - } - } - else - { - return false; - } - } - } - catch (Exception e) - { - return false; - } - - return false; - } - public bool IsMyMessage { get; set; } public string Username { get; } @@ -223,16 +158,6 @@ namespace ChatController.ChatKlassen public string OriginalImage { get; } public string OriginalImageName { get; } - - public Visibility ChatVisibility => MessageType == ChatMessageType.Message ? Visibility.Visible : Visibility.Collapsed; - - public Visibility HyperlinkVisibility => MessageType == ChatMessageType.Hyperlink ? Visibility.Visible : Visibility.Collapsed; - - public Visibility PictureVisibility => MessageType == ChatMessageType.Image ? Visibility.Visible : Visibility.Collapsed; - - public Visibility PicturePlaceHolderVisibility => MessageType == ChatMessageType.DefaultImage ? Visibility.Visible : Visibility.Collapsed; - - public Visibility DokumentVisibility => MessageType == ChatMessageType.Document ? Visibility.Visible : Visibility.Collapsed; private ImageSource _PictureSource; public ImageSource PictureSource @@ -262,7 +187,7 @@ namespace ChatController.ChatKlassen } } - public object FilePath { get; } + public string FilePath { get; } public long GroupId { get; } @@ -270,28 +195,7 @@ namespace ChatController.ChatKlassen public Guid Id { get; } - public Uri UserMessageAsUri - { - get - { - if(IsHyperlink(UserMessage)) - { - Debug.WriteLine($"Link: {UserMessage}"); - - if(!UserMessage.Contains("http")) - { - return new Uri("http://" + UserMessage); - } - - if(UserMessage.Contains("https") || UserMessage.Contains("http")) - { - return new Uri(UserMessage); - } - } - - return null; - } - } + public ImageSource ProfilePicture => OwnChatCache.GetInstance().GetUserProfilePictureFromCache(SenderId)?.ProfilePicture; protected virtual void OnPropertyChanged(string propertyName) { @@ -308,7 +212,7 @@ namespace ChatController.ChatKlassen return SendTime == message.SendTime; } - return MessageId.Equals(message.MessageId); + return Id.Equals(message.Id) && MessageId.Equals(message.MessageId); } return false; diff --git a/ChatController/ChatKlassen/Contact.cs b/ChatController/ChatKlassen/Contact.cs index 26eb9a2..74a5c02 100644 --- a/ChatController/ChatKlassen/Contact.cs +++ b/ChatController/ChatKlassen/Contact.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Windows.Media; using ChatController.LoginKlassen; @@ -82,7 +83,7 @@ namespace ChatController.ChatKlassen public bool IsChatMessageInputGridVisible { get; } - public Contact(string pName, GroupLatestMessage pLatestMessage, DateTime pTimeStamp, int pGroupId, string pUserIdManage, bool pOnlyEmployees, int pUserCount, bool isChatMessageInputGridVisible, string accentColorBrushString, string profilePicturePath, string key) + public Contact(string pName, GroupLatestMessage pLatestMessage, DateTime pTimeStamp, int pGroupId, string pUserIdManage, bool pOnlyEmployees, int pUserCount, bool isChatMessageInputGridVisible, string accentColorBrushString, string profilePicturePath, string key, Dictionary userId2Uri) { Name = pName; ReceivedMessage = pLatestMessage; @@ -108,15 +109,21 @@ namespace ChatController.ChatKlassen accentColorBrushString = "000000"; } } - + AccentColorBrush = new BrushConverter().ConvertFromString($"#{accentColorBrushString}") as SolidColorBrush; + IsGroupChat = pUserCount > 2; + + Utils.DownloadUserProfilePictures(userId2Uri); + Utils.DownloadProfilePictureAsync(profilePicturePath, key, delegate(ImageSource imageSource) { Image = imageSource; }); } + public bool IsGroupChat { get; } + public override bool Equals(object obj) { if(obj is Contact y) diff --git a/ChatController/ChatMainControl.xaml b/ChatController/ChatMainControl.xaml index 3c687ba..5a7c57d 100644 --- a/ChatController/ChatMainControl.xaml +++ b/ChatController/ChatMainControl.xaml @@ -5,6 +5,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:converter="clr-namespace:ChatController.Converter" xmlns:chatKlassen="clr-namespace:ChatController.ChatKlassen" + xmlns:core="clr-namespace:ChatController.Core" + xmlns:chatController="clr-namespace:ChatController" mc:Ignorable="d" x:Name="ChatControlling" d:DesignHeight="300" d:DesignWidth="600" Loaded="ChatMainControl_OnLoaded" @@ -13,10 +15,10 @@ + - - + diff --git a/ChatController/ChatMainControl.xaml.cs b/ChatController/ChatMainControl.xaml.cs index 6984223..7068509 100644 --- a/ChatController/ChatMainControl.xaml.cs +++ b/ChatController/ChatMainControl.xaml.cs @@ -484,21 +484,21 @@ namespace ChatController else { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[0]; - ContextItemSpeichernUnter.Visibility = Visibility.Collapsed; + var contextItemSpeichernUnter = (MenuItem)contextItems[0]; + contextItemSpeichernUnter.Visibility = Visibility.Collapsed; } if(item.PictureSource == null && item.FilePath == null) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[2]; - ContextItemSpeichernUnter.Visibility = Visibility.Visible; + var contextItemSpeichernUnter = (MenuItem)contextItems[2]; + contextItemSpeichernUnter.Visibility = Visibility.Visible; } else { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[2]; - ContextItemSpeichernUnter.Visibility = Visibility.Collapsed; + var contextItemSpeichernUnter = (MenuItem)contextItems[2]; + contextItemSpeichernUnter.Visibility = Visibility.Collapsed; } } } @@ -506,8 +506,8 @@ namespace ChatController { //Schalte speichern unter Aus var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[0]; - ContextItemSpeichernUnter.Visibility = Visibility.Collapsed; + var contextItemSpeichernUnter = (MenuItem)contextItems[0]; + contextItemSpeichernUnter.Visibility = Visibility.Collapsed; } //prüfe ob was in dem Speicher vorhanden ist //Einfügen @@ -516,40 +516,40 @@ namespace ChatController if(dataObject != null && dataObject.GetDataPresent(DataFormats.FileDrop)) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem) contextItems[1]; - ContextItemSpeichernUnter.Visibility = Visibility.Visible; + var contextItemSpeichernUnter = (MenuItem) contextItems[1]; + contextItemSpeichernUnter.Visibility = Visibility.Visible; } else if(dataObject != null && dataObject.GetDataPresent(DataFormats.Text)) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem) contextItems[1]; - ContextItemSpeichernUnter.Visibility = Visibility.Visible; + var contextItemSpeichernUnter = (MenuItem) contextItems[1]; + contextItemSpeichernUnter.Visibility = Visibility.Visible; } else if(dataObject != null && dataObject.GetDataPresent(DataFormats.Bitmap)) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem) contextItems[1]; - ContextItemSpeichernUnter.Visibility = Visibility.Visible; + var contextItemSpeichernUnter = (MenuItem) contextItems[1]; + contextItemSpeichernUnter.Visibility = Visibility.Visible; } else { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[1]; - ContextItemSpeichernUnter.Visibility = Visibility.Collapsed; + var contextItemSpeichernUnter = (MenuItem)contextItems[1]; + contextItemSpeichernUnter.Visibility = Visibility.Collapsed; } // Prüfe, ob Dokumentation erlaubt if(CurrentContact.UserIdManage == null && ChatListBox.ContextMenu.Items.Count > 3) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[3]; - ContextItemSpeichernUnter.Visibility = Visibility.Collapsed; + var contextItemSpeichernUnter = (MenuItem)contextItems[3]; + contextItemSpeichernUnter.Visibility = Visibility.Collapsed; } else if(ChatListBox.ContextMenu.Items.Count > 3) { var contextItems = ChatListBox.ContextMenu.Items; - var ContextItemSpeichernUnter = (MenuItem)contextItems[3]; - ContextItemSpeichernUnter.Visibility = Visibility.Visible; + var contextItemSpeichernUnter = (MenuItem)contextItems[3]; + contextItemSpeichernUnter.Visibility = Visibility.Visible; } if(ChatListBox.ContextMenu.Items.Count == 0) @@ -563,13 +563,13 @@ namespace ChatController { if(CurrentContact != null) { - var fileToOpen = Chat.OpenFile(); + var pathToOriginalImage = Chat.OpenFile(); - if(fileToOpen != null) + if(pathToOriginalImage != null) { - var filePath = FileUtils.ScaleImage(fileToOpen, Path.GetExtension(fileToOpen.ToUpperInvariant()), Chat.ChatDaten.MaxUploadSize); + var pathToScaledImage = FileUtils.ScaleImage(pathToOriginalImage, Path.GetExtension(pathToOriginalImage.ToUpperInvariant()), Chat.ChatDaten.MaxUploadSize); - var isFileSizeTooLarge = FileUtils.CheckFileSize(filePath, Chat.ChatDaten.MaxUploadSize); + var isFileSizeTooLarge = FileUtils.CheckFileSize(pathToScaledImage, Chat.ChatDaten.MaxUploadSize); if(isFileSizeTooLarge) { @@ -577,9 +577,9 @@ namespace ChatController return; } - if (!string.IsNullOrWhiteSpace(filePath)) + if (!string.IsNullOrWhiteSpace(pathToScaledImage)) { - _ChatMessages.AddRangeIfElementsNotIn(Chat.AddNewFile(filePath, fileToOpen, CurrentContact.GroupId, GetFirstMessage())); + _ChatMessages.AddRangeIfElementsNotIn(Chat.AddNewFile(pathToScaledImage, pathToOriginalImage, CurrentContact.GroupId, GetFirstMessage())); ChatMessages.MoveCurrentToLast(); @@ -587,11 +587,11 @@ namespace ChatController ChatListBox.ScrollIntoView(currentChatMessage); - Chat.SendFileToContact(CurrentContact, filePath,fileToOpen); + Chat.SendFileToContact(CurrentContact, pathToScaledImage,pathToOriginalImage); - if (!string.IsNullOrWhiteSpace(filePath) && !filePath.Equals(fileToOpen) && File.Exists(filePath)) + if (!string.IsNullOrWhiteSpace(pathToScaledImage) && !pathToScaledImage.Equals(pathToOriginalImage) && File.Exists(pathToScaledImage)) { - File.Delete(filePath); + File.Delete(pathToScaledImage); } } } @@ -606,8 +606,10 @@ namespace ChatController { if(CurrentContact != null && !string.IsNullOrEmpty(Chatbox.Text)) { - _ChatMessages.AddRangeIfElementsNotIn(Chat.AddNewMessage(Chatbox.Text, CurrentContact.GroupId, GetFirstMessage())); + var messages2Add = Chat.AddNewMessage(Chatbox.Text, CurrentContact.GroupId, GetFirstMessage()); + _ChatMessages.AddRangeIfElementsNotIn(messages2Add); + OnPropertyChanged(nameof(ChatMessages)); ChatMessages.MoveCurrentToLast(); ChatListBox.ScrollIntoView(ChatMessages.CurrentItem); @@ -753,7 +755,6 @@ namespace ChatController { foreach (var newContact in updatedContacts) { - if (contact.GroupId == newContact.GroupId) { // ReceivedMessage kann null sein! @@ -869,8 +870,19 @@ namespace ChatController if(selectedItem is ChatMessage selectedMessage) { - if(!string.IsNullOrEmpty(selectedMessage.OriginalImage)) + if (!string.IsNullOrEmpty(selectedMessage.OriginalImage)) { + var uri = new Uri(selectedMessage.OriginalImage); + + if(uri.IsFile) + { + var bitmapImage = new BitmapImage(uri); + bitmapImage.Freeze(); + Chat.ShowPictureWindow(bitmapImage, "Test"); + + return; + } + StartWaitingImmediately(); Chat.ShowPicture(selectedMessage.OriginalImage, CurrentContact.GroupId, (imageSource, windowTitle) => @@ -963,12 +975,6 @@ namespace ChatController }); } - private void Hyperlink_OnRequestNavigate(object sender, RequestNavigateEventArgs e) - { - Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); - e.Handled = true; - } - private void ButtonReloadGruppen_OnClick(object sender, RoutedEventArgs e) { DoSynchro(); diff --git a/ChatController/Converter/UserProfilePictureVisibilityConverter.cs b/ChatController/Converter/UserProfilePictureVisibilityConverter.cs new file mode 100644 index 0000000..cabb8bf --- /dev/null +++ b/ChatController/Converter/UserProfilePictureVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace ChatController.Converter +{ + public class UserProfilePictureVisibilityConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if(values != null && values.Length == 2 && values[0] is bool isMyMessage && values[1] is bool isGroupChat) + { + if(parameter is string test && test == "reverse") + { + return isMyMessage && isGroupChat ? Visibility.Visible : Visibility.Collapsed; + } + + return !isMyMessage && isGroupChat ? Visibility.Visible : Visibility.Collapsed; + } + + return Visibility.Collapsed; + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/ChatController/Core/BindableTextBlock.cs b/ChatController/Core/BindableTextBlock.cs new file mode 100644 index 0000000..e089037 --- /dev/null +++ b/ChatController/Core/BindableTextBlock.cs @@ -0,0 +1,31 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace ChatController.Core +{ + public class BindableTextBlock : TextBlock + { + public ObservableCollection InlineList + { + get => (ObservableCollection) GetValue(InlineListProperty); + set => SetValue(InlineListProperty, value); + } + + public static readonly DependencyProperty InlineListProperty = DependencyProperty.Register("InlineList", typeof(ObservableCollection), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged)); + + public static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if(e.NewValue == null) + { + return; + } + + var textBlock = (BindableTextBlock)sender; + + textBlock.Inlines.Clear(); + textBlock.Inlines.AddRange((ObservableCollection) e.NewValue); + } + } +} diff --git a/ChatController/Core/OwnChatCache.cs b/ChatController/Core/OwnChatCache.cs index cec0077..5699320 100644 --- a/ChatController/Core/OwnChatCache.cs +++ b/ChatController/Core/OwnChatCache.cs @@ -11,6 +11,8 @@ namespace ChatController.Core // Key-> "group-127" oder "user-938" private Dictionary _ImageSourceCache = new Dictionary(); + private Dictionary _UserProfilePictures = new Dictionary(); + private static OwnChatCache _Instance; private OwnChatCache() { } @@ -96,6 +98,41 @@ namespace ChatController.Core null; } } + + public void AddUserProfilePictureToCache(ImageSource imageSource, long userId, string uri) + { + lock(_Lock) + { + if(!_UserProfilePictures.ContainsKey(userId)) + { + _UserProfilePictures.Add(userId, new ProfilePictureObject(userId, uri, imageSource)); + } + } + } + + public ProfilePictureObject GetUserProfilePictureFromCache(long userId) + { + lock(_Lock) + { + return _UserProfilePictures.ContainsKey(userId) ? _UserProfilePictures[userId] : null; + } + } + } + + public class ProfilePictureObject + { + public long UserId { get; set; } + + public string Uri { get; set; } + + public ImageSource ProfilePicture { get; set; } + + public ProfilePictureObject(long userId, string uri, ImageSource profilePicture) + { + UserId = userId; + Uri = uri; + ProfilePicture = profilePicture; + } } public class ImageSourceCacheStorage diff --git a/ChatController/HauptKlassen/Chat.cs b/ChatController/HauptKlassen/Chat.cs index 888e8e7..0b8ba4e 100644 --- a/ChatController/HauptKlassen/Chat.cs +++ b/ChatController/HauptKlassen/Chat.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; @@ -21,7 +20,6 @@ using System.Windows.Data; using System.Windows.Forms; using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Windows.Threading; using ChatController.Core; using ChatController.Extensions; using ChatController.LoginKlassen; @@ -45,7 +43,7 @@ namespace ChatController.HauptKlassen private readonly List _Contacts = new List(); - public ChatDatenUebergabe ChatDaten { get; set; } + public static ChatDatenUebergabe ChatDaten { get; set; } private UserMessages _UserMessages; private UserMessages _AdditionalUserMessages; @@ -146,9 +144,6 @@ namespace ChatController.HauptKlassen client.ExecuteAsync(request, response => { - // TODO: Entfernen - Debug.WriteLine(response.Content); - _UserMessages = JsonConvert.DeserializeObject(response.Content); if (_UserMessages.Response.HasMorePages) @@ -297,12 +292,9 @@ namespace ChatController.HauptKlassen client.ExecuteAsync(request, response => { - // TODO: Entfernen - Debug.WriteLine(response.Content); - var messageCounter = JsonConvert.DeserializeObject(response.Content); - callback?.Invoke(messageCounter.MessageCount); + callback?.Invoke(messageCounter?.MessageCount ?? 0); }); } catch(Exception exception) @@ -357,25 +349,12 @@ namespace ChatController.HauptKlassen { var result = new List(); - if(previousMessage != null) - { - result.Add(previousMessage); - } - try { - var username = string.Empty; - - var user = ChatDaten.LoggedInUser.Response.User; - - if(user != null) - { - username = $"{user.Firstname} {user.Lastname}"; - } - var time = DateTime.Now; - result.Add(new ChatMessage(username, pMessage, time, true, pGroupId, 0, 0)); + var chatMessage = new ChatMessage(pGroupId, 0, ChatDaten.LoggedInUser.Response.User.UserName, pMessage, time, true, 0, null, null, null, null, null, ChatMessageType.Message, ChatDaten.LoggedInUser.Response.User.Oid); + result.Add(chatMessage); return AddSeparators(result); } @@ -526,81 +505,75 @@ namespace ChatController.HauptKlassen } } - public List AddNewFile(string pFile, string pOriginalFilePath, long pGroupId, ChatMessage previousMessage) + public List AddNewFile(string pathToFile, string originalFilePath, long groupId, ChatMessage previousMessage) { var result = new List(); - if (previousMessage != null) - { - result.Add(previousMessage); - } - try { - if (string.IsNullOrEmpty(pFile) || string.IsNullOrEmpty(pOriginalFilePath)) + if (string.IsNullOrEmpty(pathToFile) || string.IsNullOrEmpty(originalFilePath)) { return result; } - var message = _UserMessages.Response.Messages.Last(); + var latestMessage = _UserMessages.Response.Messages.Last(); var sendTime = DateTime.Now; - if (Constants.ImageFileExtensions.Contains(Path.GetExtension(pFile).ToUpperInvariant())) + if (Constants.ImageFileExtensions.Contains(Path.GetExtension(pathToFile).ToUpperInvariant())) { byte[] file; - using (Stream reader = File.OpenRead(pFile)) + using (Stream reader = File.OpenRead(pathToFile)) { file = Utils.ReadFully(reader); } - - var memoryStream = new MemoryStream(file); - var image = Image.FromStream(memoryStream); - - using (var bitmap = new Bitmap(image)) + + using(var memoryStream = new MemoryStream(file)) { - using (var stream = new MemoryStream()) + var image = Image.FromStream(memoryStream); + + using (var bitmap = new Bitmap(image)) { - short orient = 0; - const int orientationId = 0x0112; - - if (image.PropertyIdList.Contains(orientationId)) + using (var stream = new MemoryStream()) { - var item = image.GetPropertyItem(orientationId); + short orient = 0; + const int orientationId = 0x0112; - orient = BitConverter.ToInt16(item.Value, 0); + if (image.PropertyIdList.Contains(orientationId)) + { + var item = image.GetPropertyItem(orientationId); + + orient = BitConverter.ToInt16(item.Value, 0); - bitmap.SetPropertyItem(item); + bitmap.SetPropertyItem(item); + } + + var flipType = Utils.OrientationToFlipType(orient.ToString()); + + bitmap.RotateFlip(flipType); + + bitmap.Save(stream, ImageFormat.Jpeg); + + + var bitmapImage = new BitmapImage(); + + bitmapImage.BeginInit(); + bitmapImage.StreamSource = new MemoryStream(stream.ToArray()); + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + var chatMessage = new ChatMessage(groupId, 0, ChatDaten.LoggedInUser.Response.User.UserName, Path.GetFileName(pathToFile), sendTime, true, memoryStream.Length, null, pathToFile, Path.GetFileName(pathToFile), null, bitmapImage, ChatMessageType.Image, ChatDaten.LoggedInUser.Response.User.Oid); + result.Add(chatMessage); } - - var flipType = Utils.OrientationToFlipType(orient.ToString()); - - bitmap.RotateFlip(flipType); - - bitmap.Save(stream, ImageFormat.Jpeg); - - - var bitmapImage = new BitmapImage(); - - bitmapImage.BeginInit(); - bitmapImage.StreamSource = new MemoryStream(stream.ToArray()); - bitmapImage.EndInit(); - bitmapImage.Freeze(); - - var filename = Path.GetFileName(pOriginalFilePath).ToLower(); - - filename = filename.Replace(".bmp", ".jpg"); - - result.Add(new ChatMessage(message.User_Name, filename, sendTime, true, bitmapImage, pFile, filename, pGroupId, file.LongLength, message.Id)); } } - - memoryStream.Dispose(); - memoryStream.Close(); } else { - result.Add(new ChatMessage(message.User_Name, Path.GetFileName(pFile), sendTime, message.Smaller_Image, true, pFile, pGroupId, 0, message.Id)); + var linkToThumbnail = $"{ChatDaten.ServerUrl}/document.png"; + + var chatMessage = new ChatMessage(groupId, 0, ChatDaten.LoggedInUser.Response.User.UserName, Path.GetFileName(pathToFile), sendTime, true, 0, pathToFile, null, null, linkToThumbnail, null, ChatMessageType.Document, ChatDaten.LoggedInUser.Response.User.Oid); + result.Add(chatMessage); } return AddSeparators(result); @@ -613,11 +586,11 @@ namespace ChatController.HauptKlassen return result; } - public List AddMediumContextDerNachrichtView(byte[] pFile, string pFileName, long pGroupId, ChatMessage previousMessage) + public List AddFileToMessage(byte[] file, string fileName, long groupId, ChatMessage previousMessage) { var result = new List(); - if (string.IsNullOrEmpty(pFileName)) + if (string.IsNullOrEmpty(fileName)) { return result; } @@ -631,10 +604,10 @@ namespace ChatController.HauptKlassen var sendtime = DateTime.Now; - if (Constants.ImageFileExtensions.Contains(Path.GetExtension(pFileName).ToUpperInvariant())) + if (Constants.ImageFileExtensions.Contains(Path.GetExtension(fileName).ToUpperInvariant())) { var bitmapImage = new BitmapImage(); - var memoryStream = new MemoryStream(pFile); + var memoryStream = new MemoryStream(file); bitmapImage.BeginInit(); bitmapImage.StreamSource = memoryStream; @@ -644,11 +617,16 @@ namespace ChatController.HauptKlassen memoryStream.Close(); ImageSource logo = bitmapImage; - result.Add(new ChatMessage(message.User_Name, Path.GetFileName(pFileName), sendtime, true, logo, pFileName, Path.GetFileName(pFileName), pGroupId, memoryStream.Length, message.Id)); + // Mit Logo + var chatMessage = new ChatMessage(groupId, 0, ChatDaten.LoggedInUser.Response.User.UserName, Path.GetFileName(fileName), sendtime, true, memoryStream.Length, null, fileName, Path.GetFileName(fileName), null, logo, ChatMessageType.Image, ChatDaten.LoggedInUser.Response.User.Oid); + + result.Add(chatMessage); } else { - result.Add(new ChatMessage(message.User_Name, Path.GetFileName(pFileName), sendtime, message.Smaller_Image, true, pFileName, pGroupId, 0, message.Id)); + // Dokument + var chatMessage = new ChatMessage(groupId, 0, ChatDaten.LoggedInUser.Response.User.UserName, Path.GetFileName(fileName), sendtime, true, 0, fileName, null, null, message.Smaller_Image, null, ChatMessageType.Document, ChatDaten.LoggedInUser.Response.User.Oid); + result.Add(chatMessage); } return AddSeparators(result); @@ -847,7 +825,7 @@ namespace ChatController.HauptKlassen var mediaFile = File.ReadAllBytes(fileList[0]); - pChatMainControl.AddMessages(AddMediumContextDerNachrichtView(mediaFile, fileName, pGroupOid, pChatMainControl.GetFirstMessage())); + pChatMainControl.AddMessages(AddFileToMessage(mediaFile, fileName, pGroupOid, pChatMainControl.GetFirstMessage())); CollectionViewSource.GetDefaultView(pChatMainControl.ChatListBox.ItemsSource).Refresh(); @@ -886,7 +864,7 @@ namespace ChatController.HauptKlassen Image image = bitmap; - pChatMainControl.AddMessages(AddMediumContextDerNachrichtView(Utils.ImageToByteArray(image), imageName, pGroupOid, pChatMainControl.GetFirstMessage())); + pChatMainControl.AddMessages(AddFileToMessage(Utils.ImageToByteArray(image), imageName, pGroupOid, pChatMainControl.GetFirstMessage())); CollectionViewSource.GetDefaultView(pChatMainControl.ChatListBox.ItemsSource).Refresh(); @@ -1095,6 +1073,8 @@ namespace ChatController.HauptKlassen var key = $"group-{groupInput.Oid}"; + + contacts.Add(new Contact( groupInput.Name, groupInput.LastMessage, @@ -1106,7 +1086,7 @@ namespace ChatController.HauptKlassen groupInput.CanWrite, groupInput.AccentColor, groupInput.Avatar, - key)); + key, groupInput.Users.ToDictionary(user => user.Oid, user => user.Picture))); if(pShouldUpdateLastTimeStamp) { @@ -1129,11 +1109,6 @@ namespace ChatController.HauptKlassen { var result = new List(); - if(previousMessage != null) - { - result.Add(previousMessage); - } - foreach (var message in pMessages) { var time = DateTime.Parse(message.Timestamp.Date); @@ -1145,16 +1120,16 @@ namespace ChatController.HauptKlassen if (!string.IsNullOrEmpty(message.File)) { var message2Add = Constants.ImageFileExtensions.Contains(Path.GetExtension(message.File).ToUpperInvariant()) ? //SmallerImage ist null - new ChatMessage(message.User_Name, messageText, formattedTime, isLoggedInUsersMessage, message.Smaller_Image, message.File, message.Original_Filename, message.GroupId, message.FileSize, message.Id) : - new ChatMessage(message.User_Name, messageText, formattedTime, message.Smaller_Image, isLoggedInUsersMessage, message.File, message.GroupId, message.FileSize, message.Id); + new ChatMessage(message.GroupId, message.Id, message.User_Name, messageText, formattedTime, isLoggedInUsersMessage, message.FileSize, message.Smaller_Image, message.File, message.Original_Filename, null, null, ChatMessageType.Image, message.UserId) : + new ChatMessage(message.GroupId, message.Id, message.User_Name, messageText, formattedTime, isLoggedInUsersMessage, 0, message.File, null, null, message.Smaller_Image, null, ChatMessageType.Document, message.UserId); result.AddIfNotIn(message2Add); } else { - var message2Add2 = new ChatMessage(message.User_Name, message.Text, formattedTime, isLoggedInUsersMessage, message.GroupId, message.FileSize, message.Id); - - result.AddIfNotIn(message2Add2); + var chatMessage = new ChatMessage(message.GroupId, message.Id, message.User_Name, message.Text, formattedTime, isLoggedInUsersMessage, message.FileSize, null, null, null, null, null, ChatMessageType.Message, message.UserId); + + result.AddIfNotIn(chatMessage); } } @@ -1174,7 +1149,7 @@ namespace ChatController.HauptKlassen { if(message.SendTime.Date != previousMessage.SendTime.Date) { - var separatorMessage = new ChatMessage(null, null, message.SendTime.Date, false, 0, 0, -1) { IsSeparator = true }; + var separatorMessage = new ChatMessage(0, -1, null, null, message.SendTime.Date, false, 0, null, null, null, null, null, ChatMessageType.Separator, 0); separators.Add(separatorMessage); diff --git a/ChatController/LoginControl.xaml b/ChatController/LoginControl.xaml index 3fda31a..49a8ad7 100644 --- a/ChatController/LoginControl.xaml +++ b/ChatController/LoginControl.xaml @@ -4,6 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d"> + + + @@ -48,12 +51,30 @@ diff --git a/ChatController/LoginControl.xaml.cs b/ChatController/LoginControl.xaml.cs index 6cfae31..2342200 100644 --- a/ChatController/LoginControl.xaml.cs +++ b/ChatController/LoginControl.xaml.cs @@ -92,10 +92,20 @@ namespace ChatController LeseDateiFallsVorhanden(); #if DEBUG + //_Kundennummer = "4368658436"; + //_ChatCode = "ZMCKb-BckTt"; + //_Benutzername = "muellerp"; + //Password = "F5aTk9Co47JFJCWB2Z7Y"; + + //_Kundennummer = "9988776655"; + //_ChatCode = "K2UPV-qrirb"; + //_Benutzername = "lyndon"; + //Password = "lyndon2travemünde"; + _Kundennummer = "4368658436"; - _ChatCode = "ZMCKb-BckTt"; - _Benutzername = "muellerp"; - Password = "F5aTk9Co47JFJCWB2Z7Y"; + _ChatCode = "B7x0b-FhhJA"; + _Benutzername = "jettenl"; + Password = "n8-yR+3Q=6nX#"; #endif } diff --git a/ChatController/LoginKlassen/UserDaten.cs b/ChatController/LoginKlassen/UserDaten.cs index 5ac46ef..1c41b9e 100644 --- a/ChatController/LoginKlassen/UserDaten.cs +++ b/ChatController/LoginKlassen/UserDaten.cs @@ -53,5 +53,25 @@ namespace ChatController.LoginKlassen public bool IsEmployee { get; set; } public string Token { get; set; } + + public string UserName + { + get + { + var result = string.Empty; + + if(!string.IsNullOrWhiteSpace(Firstname)) + { + result = $"{Firstname} "; + } + + if(!string.IsNullOrWhiteSpace(Lastname)) + { + result += Lastname; + } + + return result; + } + } } } diff --git a/ChatController/Utilities/Utils.cs b/ChatController/Utilities/Utils.cs index 4519508..08b1f7c 100644 --- a/ChatController/Utilities/Utils.cs +++ b/ChatController/Utilities/Utils.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -7,7 +9,9 @@ using System.Linq; using System.Net; using System.Reflection; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using System.Windows; +using System.Windows.Documents; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -38,9 +42,16 @@ namespace ChatController.Utilities return; } - var url = pathToProfilePicture; - - var client = new RestClient(url); + DownloadImage(pathToProfilePicture, imageSource => + { + cache.AddProfilePictureToCache(key, imageSource, pathToProfilePicture); + callback?.Invoke(imageSource); + }); + } + + private static void DownloadImage(string uri, Action callback) + { + var client = new RestClient(uri); var request = new RestRequest { ResponseWriter = stream => @@ -58,8 +69,6 @@ namespace ChatController.Utilities bitmap.EndInit(); bitmap.Freeze(); - cache.AddProfilePictureToCache(key, bitmap, pathToProfilePicture); - callback?.Invoke(bitmap); } }; @@ -70,6 +79,33 @@ namespace ChatController.Utilities client.ExecuteAsync(request, response => { /*Das übernimmt der ResponseWriter von oben*/ }); } + public static void DownloadUserProfilePictures(Dictionary userId2Uris) + { + var cache = OwnChatCache.GetInstance(); + + foreach(var id2Uri in userId2Uris) + { + var userProfilePicture = cache.GetUserProfilePictureFromCache(id2Uri.Key); + + if(userProfilePicture == null) + { + try + { + var uri = id2Uri.Value; + + DownloadImage(uri, profilePicture => + { + cache.AddUserProfilePictureToCache(profilePicture, id2Uri.Key, uri); + }); + } + catch(Exception) + { + + } + } + } + } + public static void DownloadImageAsync(string uri, string key, CacheCategory cacheCategory, Action callback) { if(string.IsNullOrEmpty(uri)) @@ -88,35 +124,11 @@ namespace ChatController.Utilities return; } - var url = uri; - var client = new RestClient(url); - var request = new RestRequest + DownloadImage(uri, imageSource => { - ResponseWriter = stream => - { - var bitmap = new BitmapImage(); - - var localStream = new MemoryStream(); - - stream.CopyTo(localStream); - - localStream.Position = 0; - - bitmap.BeginInit(); - bitmap.StreamSource = localStream; - bitmap.EndInit(); - bitmap.Freeze(); - - cache.AddImageToCache(key, bitmap, cacheCategory, uri); - - callback?.Invoke(bitmap); - } - }; - - request.AddHeader(Constants.Token, AuthToken); - request.AddHeader(Constants.CustomerId, Tenant); - - client.ExecuteAsync(request, response => { }); + cache.AddImageToCache(key, imageSource, cacheCategory, uri); + callback?.Invoke(imageSource); + }); } private static ImageSource GetDefaultImageSource(bool pIsGroup, bool pIsEmployee) @@ -438,5 +450,82 @@ namespace ChatController.Utilities return 100; } + + private static readonly Regex _UrlRegex = new Regex(@"(?#Protocol)(http(?:s?)\:(\/\/|\\\\)|(w){3}(2|3)?\.{1})(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public static ObservableCollection ConvertLinksToHyperlinks(string messageText) + { + var result = new ObservableCollection(); + + if(messageText == null) + { + return result; + } + + var guidString = Guid.NewGuid().ToString(); + var replacementString = Guid.NewGuid().ToString(); + + var matches = _UrlRegex.Matches(messageText); + + var index2Link = new Dictionary(); + + for(var i = 0; i < matches.Count; i++) + { + var link = matches[i].Value; + + if(!link.Contains("http")) + { + link = $"http://{link}"; + } + + var hyperlink = new Hyperlink(new Run(link)) + { + NavigateUri = new Uri(link, UriKind.Absolute) + }; + + hyperlink.RequestNavigate += (s, e) => + { + Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri)); + e.Handled = true; + }; + + index2Link[i] = hyperlink; + } + + var splitMessage = _UrlRegex.Replace(messageText, replacementString + guidString).Split(new[] {guidString}, StringSplitOptions.None); + + var linkIndex = 0; + foreach(var text in splitMessage) + { + if(text.Equals(replacementString)) + { + result.Add(index2Link[linkIndex]); + linkIndex++; + } + else if(text.StartsWith(replacementString) || text.EndsWith(replacementString)) + { + var splitLine = text.Split(new[] { replacementString }, StringSplitOptions.None); + + foreach(var partLine in splitLine) + { + if(string.Empty.Equals(partLine)) + { + result.Add(index2Link[linkIndex]); + linkIndex++; + } + else + { + result.Add(new Run(partLine)); + } + } + } + else + { + result.Add(new Run(text)); + } + } + + return result; + } } } diff --git a/PrototypChat/.vs/PrototypChat/v16/.suo b/PrototypChat/.vs/PrototypChat/v16/.suo index 06aa56a3815e09d2f3d0464b9d523df4ccdafcfc..e1984218a243ca5b13bf2db1937308b2f5701833 100644 GIT binary patch delta 12841 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 delta 8862 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