├── .gitignore ├── code ├── Launcher │ ├── App.ico │ ├── Launcher.csproj │ ├── Launcher.settings.xml │ └── Program.cs ├── Links │ ├── LinkClient.cs │ ├── LinkCrypto.cs │ ├── LinkError.cs │ ├── LinkEventArgs.cs │ ├── LinkException.cs │ ├── LinkExtension.cs │ ├── LinkListener.cs │ ├── LinkNotice.cs │ ├── LinkNoticeSource.cs │ ├── LinkPacket.cs │ ├── Links.cs │ ├── Links.csproj │ └── LinksHelper.cs ├── Logger │ ├── Log.cs │ ├── LogListener.cs │ └── Logger.csproj ├── Messenger.sln └── Messenger │ ├── App.ico │ ├── App.manifest │ ├── App.xaml │ ├── App.xaml.cs │ ├── Chatter.xaml │ ├── Chatter.xaml.cs │ ├── Commands.cs │ ├── Connection.xaml │ ├── Connection.xaml.cs │ ├── ControlProfileImage.xaml │ ├── ControlProfileImage.xaml.cs │ ├── ControlShare.xaml │ ├── ControlShare.xaml.cs │ ├── ControlShareWorker.xaml │ ├── ControlShareWorker.xaml.cs │ ├── Controllers │ ├── MessageController.cs │ ├── ProfileController.cs │ └── ShareController.cs │ ├── Entrance.xaml │ ├── Entrance.xaml.cs │ ├── Extensions │ ├── Extension.cs │ └── NativeMethods.cs │ ├── Framework.cs │ ├── Messenger.csproj │ ├── Models │ ├── Host.cs │ ├── IFinal.cs │ ├── LoaderAttribute.cs │ ├── LoaderFlags.cs │ ├── Packet.cs │ ├── Profile.cs │ ├── RouteAttribute.cs │ ├── Share.cs │ ├── ShareBasic.cs │ ├── ShareReceiver.cs │ ├── ShareStatus.cs │ └── ShareWorker.cs │ ├── Modules │ ├── CacheModule.cs │ ├── EnvironmentModule.cs │ ├── HistoryModule.cs │ ├── HostModule.cs │ ├── LinkModule.cs │ ├── PostModule.cs │ ├── ProfileModule.cs │ ├── RouteModule.cs │ ├── SettingModule.cs │ └── ShareModule.cs │ ├── PageClient.xaml │ ├── PageClient.xaml.cs │ ├── PageFrame.xaml │ ├── PageFrame.xaml.cs │ ├── PageGroups.xaml │ ├── PageGroups.xaml.cs │ ├── PageManager.cs │ ├── PageOption.xaml │ ├── PageOption.xaml.cs │ ├── PageProfile.xaml │ ├── PageProfile.xaml.cs │ ├── PageRecent.xaml │ ├── PageRecent.xaml.cs │ ├── PageShare.xaml │ ├── PageShare.xaml.cs │ ├── Resources │ ├── Collection.xaml │ ├── Container.xaml │ ├── General.xaml │ ├── Geometry.xaml │ └── Validation.xaml │ ├── Shower.xaml │ ├── Shower.xaml.cs │ └── Tools │ ├── ImageSourceConverter.cs │ ├── LengthUnitConverter.cs │ ├── LogicToPixelConverter.cs │ ├── ObjectTypeConverter.cs │ ├── ProfileHintConverter.cs │ ├── ProfileIdValidation.cs │ ├── ProfileLogoConverter.cs │ ├── SocketPortValidation.cs │ └── StringEmptyConverter.cs └── image ├── 00.png ├── 01.png ├── 02.png └── 03.png /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /code/Launcher/App.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/code/Launcher/App.ico -------------------------------------------------------------------------------- /code/Launcher/Launcher.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | netcoreapp3.1;net472 6 | Launcher 7 | Launcher 8 | 8.0 9 | App.ico 10 | Messenger Project 11 | Mikodev 12 | Mikodev 2020 13 | 1.9.0 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Always 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /code/Launcher/Launcher.settings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /code/Launcher/Program.cs: -------------------------------------------------------------------------------- 1 | using Mikodev.Logger; 2 | using Mikodev.Network; 3 | using System; 4 | using System.Collections; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | 10 | namespace Launcher 11 | { 12 | internal class Program 13 | { 14 | private static async Task Main(string[] args) 15 | { 16 | Log.Run(nameof(Launcher) + ".log"); 17 | 18 | try 19 | { 20 | var xml = new XmlDocument(); 21 | xml.Load(nameof(Launcher) + ".settings.xml"); 22 | var lst = xml.SelectNodes("/settings/setting[@key]"); 23 | var dic = ((IEnumerable)lst) 24 | .Cast() 25 | .ToDictionary(r => r.SelectSingleNode("@key").Value, r => r.SelectSingleNode("@value").Value); 26 | var nam = dic["server-name"]; 27 | var add = IPAddress.Parse(dic["listen-address"]); 28 | var max = int.Parse(dic["client-limits"]); 29 | var pot = int.Parse(dic["tcp-port"]); 30 | var bro = int.Parse(dic["udp-port"]); 31 | await LinkListener.Run(add, pot, bro, max, nam); 32 | } 33 | catch (Exception ex) 34 | { 35 | Log.Error(ex); 36 | } 37 | 38 | Log.Close(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /code/Links/LinkCrypto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace Mikodev.Network 6 | { 7 | internal static class LinkCrypto 8 | { 9 | internal const int _Key = 32; 10 | 11 | internal const int _Block = 16; 12 | 13 | internal readonly static Random s_ran = new Random(); 14 | 15 | public static byte[] GetKey() 16 | { 17 | var buf = new byte[_Key]; 18 | lock (s_ran) 19 | s_ran.NextBytes(buf); 20 | return buf; 21 | } 22 | 23 | public static byte[] GetBlock() 24 | { 25 | var buf = new byte[_Block]; 26 | lock (s_ran) 27 | s_ran.NextBytes(buf); 28 | return buf; 29 | } 30 | 31 | public static byte[] Encrypt(this AesManaged aes, byte[] buf) => Transform(buf, 0, buf.Length, aes.CreateEncryptor()); 32 | 33 | public static byte[] Decrypt(this AesManaged aes, byte[] buf) => Transform(buf, 0, buf.Length, aes.CreateDecryptor()); 34 | 35 | internal static byte[] Transform(byte[] buffer, int offset, int count, ICryptoTransform tramsform) 36 | { 37 | var mst = new MemoryStream(); 38 | using (var cst = new CryptoStream(mst, tramsform, CryptoStreamMode.Write)) 39 | cst.Write(buffer, offset, count); 40 | return mst.ToArray(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /code/Links/LinkError.cs: -------------------------------------------------------------------------------- 1 | namespace Mikodev.Network 2 | { 3 | public enum LinkError : int 4 | { 5 | None, 6 | 7 | Success, 8 | 9 | Overflow, 10 | 11 | ProtocolMismatch, 12 | 13 | IdInvalid, 14 | 15 | IdConflict, 16 | 17 | CountLimited, 18 | 19 | GroupLimited, 20 | 21 | QueueLimited, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/Links/LinkEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Mikodev.Network 4 | { 5 | public class LinkEventArgs : EventArgs 6 | { 7 | internal T _obj; 8 | 9 | internal bool _cancel = false; 10 | 11 | internal bool _finish = false; 12 | 13 | public LinkEventArgs(T value) => _obj = value; 14 | 15 | public T Object => _obj; 16 | 17 | public bool Cancel { get => _cancel; set => _cancel = value; } 18 | 19 | public bool Finish { get => _finish; set => _finish = value; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/Links/LinkException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Mikodev.Network 5 | { 6 | [Serializable] 7 | public class LinkException : Exception 8 | { 9 | internal readonly LinkError _error = LinkError.None; 10 | 11 | public LinkException(LinkError error) : base(_GetMessage(error)) => _error = error; 12 | 13 | public LinkException(LinkError error, Exception inner) : base(_GetMessage(error), inner) => _error = error; 14 | 15 | protected LinkException(SerializationInfo info, StreamingContext context) : base(info, context) 16 | { 17 | if (info == null) 18 | throw new ArgumentNullException(nameof(info)); 19 | _error = (LinkError)info.GetValue(nameof(LinkError), typeof(LinkError)); 20 | } 21 | 22 | public override void GetObjectData(SerializationInfo info, StreamingContext context) 23 | { 24 | if (info == null) 25 | throw new ArgumentNullException(nameof(info)); 26 | info.AddValue(nameof(LinkError), _error); 27 | base.GetObjectData(info, context); 28 | } 29 | 30 | internal static string _GetMessage(LinkError error) 31 | { 32 | switch (error) 33 | { 34 | case LinkError.Success: 35 | return "Operation successful without error!"; 36 | 37 | case LinkError.Overflow: 38 | return "Buffer length overflow!"; 39 | 40 | case LinkError.ProtocolMismatch: 41 | return "Protocol mismatch!"; 42 | 43 | case LinkError.IdInvalid: 44 | return "Invalid client id!"; 45 | 46 | case LinkError.IdConflict: 47 | return "Client id conflict with current users!"; 48 | 49 | case LinkError.CountLimited: 50 | return "Client count has been limited!"; 51 | 52 | case LinkError.GroupLimited: 53 | return "Group label is too many!"; 54 | 55 | case LinkError.QueueLimited: 56 | return "Message queue full!"; 57 | 58 | default: 59 | return "Unknown error!"; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /code/Links/LinkExtension.cs: -------------------------------------------------------------------------------- 1 | using Mikodev.Binary; 2 | using System; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using static System.BitConverter; 8 | 9 | namespace Mikodev.Network 10 | { 11 | public static class LinkExtension 12 | { 13 | public static byte[] Concat(params byte[][] arrays) 14 | { 15 | var sum = 0; 16 | for (var i = 0; i < arrays.Length; i++) 17 | sum += arrays[i].Length; 18 | var arr = new byte[sum]; 19 | var idx = 0; 20 | for (var i = 0; i < arrays.Length; i++) 21 | { 22 | var cur = arrays[i]; 23 | var len = cur.Length; 24 | Buffer.BlockCopy(cur, 0, arr, idx, len); 25 | idx += len; 26 | } 27 | return arr; 28 | } 29 | 30 | public static Task ConnectAsyncEx(this Socket socket, EndPoint endpoint) => Task.Factory.FromAsync((arg, obj) => socket.BeginConnect(endpoint, arg, obj), socket.EndConnect, null); 31 | 32 | public static Task AcceptAsyncEx(this Socket socket) => Task.Factory.FromAsync(socket.BeginAccept, socket.EndAccept, null); 33 | 34 | public static int SetKeepAlive(this Socket socket, bool enable = true, uint before = Links.KeepAliveBefore, uint interval = Links.KeepAliveInterval) 35 | { 36 | if (enable == true && (before < 1 || interval < 1)) 37 | throw new ArgumentOutOfRangeException("Keep alive argument out of range."); 38 | var val = new byte[sizeof(uint)]; 39 | var res = Concat(GetBytes(1U), GetBytes(before), GetBytes(interval)); 40 | _ = socket.IOControl(IOControlCode.KeepAliveValues, res, val); 41 | return ToInt32(val, 0); 42 | } 43 | 44 | public static async Task ReceiveAsyncExt(this Socket socket) 45 | { 46 | var buf = await ReceiveAsyncEx(socket, sizeof(int)); 47 | var len = ToInt32(buf, 0); 48 | var res = await ReceiveAsyncEx(socket, len); 49 | return res; 50 | } 51 | 52 | public static async Task ReceiveAsyncEx(this Socket socket, int length) 53 | { 54 | if (length < 1 || length > Links.BufferLengthLimit) 55 | throw new LinkException(LinkError.Overflow); 56 | var offset = 0; 57 | var buffer = new byte[length]; 58 | while (length > 0) 59 | { 60 | var res = await Task.Factory.FromAsync((a, s) => socket.BeginReceive(buffer, offset, length, SocketFlags.None, a, s), socket.EndReceive, null); 61 | if (res < 1) 62 | throw new SocketException((int)SocketError.ConnectionReset); 63 | offset += res; 64 | length -= res; 65 | } 66 | return buffer; 67 | } 68 | 69 | public static async Task SendAsyncExt(this Socket socket, byte[] buffer) 70 | { 71 | var len = GetBytes(buffer.Length); 72 | await SendAsyncEx(socket, len); 73 | await SendAsyncEx(socket, buffer); 74 | } 75 | 76 | public static Task SendAsyncEx(this Socket socket, byte[] buffer) => SendAsyncEx(socket, buffer, 0, buffer.Length); 77 | 78 | public static async Task SendAsyncEx(this Socket socket, byte[] buffer, int offset, int length) 79 | { 80 | while (length > 0) 81 | { 82 | var res = await Task.Factory.FromAsync((a, o) => socket.BeginSend(buffer, offset, length, SocketFlags.None, a, o), socket.EndSend, null); 83 | if (res < 1) 84 | throw new SocketException((int)SocketError.ConnectionReset); 85 | offset += res; 86 | length -= res; 87 | } 88 | } 89 | 90 | public static LinkPacket LoadValue(this LinkPacket src, byte[] buf) 91 | { 92 | var ori = new Token(LinksHelper.Generator, buf); 93 | src._buffer = buf; 94 | src._origin = ori; 95 | src._source = ori["source"].As(); 96 | src._target = ori["target"].As(); 97 | src._path = ori["path"].As(); 98 | src._data = ori["data", nothrow: true]; 99 | return src; 100 | } 101 | 102 | public static async Task TimeoutAfter(this Task task, string message = null, int milliseconds = Links.Timeout) 103 | { 104 | if (task == null) 105 | throw new ArgumentNullException(nameof(task)); 106 | if (milliseconds < 0) 107 | throw new ArgumentOutOfRangeException(nameof(milliseconds)); 108 | using (var src = new CancellationTokenSource()) 109 | { 110 | var res = await Task.WhenAny(task, Task.Delay(milliseconds)); 111 | if (res != task) 112 | throw (string.IsNullOrEmpty(message)) 113 | ? new TimeoutException() 114 | : new TimeoutException(message); 115 | src.Cancel(); 116 | await task; 117 | } 118 | } 119 | 120 | public static async Task TimeoutAfter(this Task task, string message = null, int milliseconds = Links.Timeout) 121 | { 122 | if (task == null) 123 | throw new ArgumentNullException(nameof(task)); 124 | if (milliseconds < 0) 125 | throw new ArgumentOutOfRangeException(nameof(milliseconds)); 126 | using (var src = new CancellationTokenSource()) 127 | { 128 | var res = await Task.WhenAny(task, Task.Delay(milliseconds)); 129 | if (res != task) 130 | throw (string.IsNullOrEmpty(message)) 131 | ? new TimeoutException() 132 | : new TimeoutException(message); 133 | src.Cancel(); 134 | return await task; 135 | } 136 | } 137 | 138 | public static void AssertFatal(this bool result, string message) 139 | { 140 | if (result) 141 | return; 142 | Environment.FailFast(message); 143 | } 144 | 145 | public static void AssertFatal(this bool result, bool assert, string message) 146 | { 147 | if (result && assert) 148 | return; 149 | Environment.FailFast(message); 150 | } 151 | 152 | public static void AssertError(this LinkError error) 153 | { 154 | if (error == LinkError.Success) 155 | return; 156 | throw new LinkException(error); 157 | } 158 | 159 | /// 160 | /// 显式放弃等待该任务 161 | /// 162 | public static void Ignore(this T task) where T : Task 163 | { 164 | return; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /code/Links/LinkNotice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Mikodev.Network 5 | { 6 | public class LinkNotice 7 | { 8 | internal readonly LinkNoticeSource _source; 9 | 10 | internal readonly int _value; 11 | 12 | internal int _status; 13 | 14 | public bool IsAny => _status > 0; 15 | 16 | internal LinkNotice() => _status = 0; 17 | 18 | internal LinkNotice(LinkNoticeSource inspector, int version) 19 | { 20 | _source = inspector; 21 | _value = version; 22 | _status = 1; 23 | } 24 | 25 | public void Handled() 26 | { 27 | // 阻止多次调用 28 | if (Interlocked.CompareExchange(ref _status, 2, 1) != 1) 29 | throw new InvalidOperationException("Invalid operation!"); 30 | _source._Handled(_value); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/Links/LinkNoticeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Mikodev.Network 5 | { 6 | public class LinkNoticeSource 7 | { 8 | internal readonly TimeSpan _interval; 9 | 10 | internal DateTime _timestamp = DateTime.MinValue; 11 | 12 | internal int _version = 0; 13 | 14 | internal int _handled = 0; 15 | 16 | public LinkNoticeSource(TimeSpan interval) 17 | { 18 | if (interval < TimeSpan.Zero) 19 | throw new ArgumentOutOfRangeException(nameof(interval)); 20 | _interval = interval; 21 | } 22 | 23 | public void Update() 24 | { 25 | _ = Interlocked.Increment(ref _version); 26 | } 27 | 28 | public LinkNotice Notice() => GetNotice(false); 29 | 30 | public LinkNotice GetNotice(bool force) 31 | { 32 | var ver = Volatile.Read(ref _version); 33 | var cur = _handled; 34 | if (cur == ver) 35 | goto nothing; 36 | 37 | if (force) 38 | return new LinkNotice(this, _version); 39 | 40 | var old = _timestamp; 41 | var now = DateTime.Now; 42 | var sub = now - old; 43 | if (sub > TimeSpan.Zero && sub < _interval) 44 | goto nothing; 45 | return new LinkNotice(this, ver); 46 | 47 | nothing: 48 | return new LinkNotice(); 49 | } 50 | 51 | internal void _Handled(int version) 52 | { 53 | var now = DateTime.Now; 54 | _timestamp = now; 55 | Volatile.Write(ref _handled, version); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /code/Links/LinkPacket.cs: -------------------------------------------------------------------------------- 1 | using Mikodev.Binary; 2 | 3 | namespace Mikodev.Network 4 | { 5 | public class LinkPacket 6 | { 7 | internal int _source = 0; 8 | 9 | internal int _target = 0; 10 | 11 | internal string _path = null; 12 | 13 | internal byte[] _buffer = null; 14 | 15 | internal Token _origin = null; 16 | 17 | internal Token _data = null; 18 | 19 | public int Source => _source; 20 | 21 | public int Target => _target; 22 | 23 | public string Path => _path; 24 | 25 | public byte[] Buffer => _buffer; 26 | 27 | public Token Origin => _origin; 28 | 29 | public Token Data => _data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /code/Links/Links.cs: -------------------------------------------------------------------------------- 1 | namespace Mikodev.Network 2 | { 3 | public static class Links 4 | { 5 | public const string Protocol = "mikodev.messenger.v1.20"; 6 | 7 | public const int Port = 7550; 8 | 9 | public const int BroadcastPort = Port; 10 | 11 | public const int Timeout = 5 * 1000; 12 | 13 | public const int KeepAliveBefore = 300 * 1000; 14 | 15 | public const int KeepAliveInterval = Timeout; 16 | 17 | public const int Id = 0; 18 | 19 | public const int DefaultId = 0x7FFFFFFF; 20 | 21 | public const int ServerSocketLimit = 256; 22 | 23 | public const int ClientSocketLimit = 64; 24 | 25 | public const int BufferLength = 4 * 1024; 26 | 27 | public const int BufferLengthLimit = 1024 * 1024; 28 | 29 | public const int BufferQueueLimit = 16 * 1024 * 1024; 30 | 31 | public const int Delay = 4; 32 | 33 | public const int GroupLabelLimit = 32; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/Links/Links.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Mikodev.Links 6 | Mikodev.Network 7 | Messenger Project 8 | Mikodev 9 | Mikodev 2020 10 | 1.9.0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /code/Links/LinksHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Mikodev.Network 2 | { 3 | public static class LinksHelper 4 | { 5 | public static readonly Binary.IGenerator Generator = Binary.Generator.CreateDefault(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /code/Logger/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Mikodev.Logger 11 | { 12 | public static class Log 13 | { 14 | private const int _MaxQueueLength = 256; 15 | 16 | /// 17 | /// 日志固定前缀 (防止循环记录日志) 18 | /// 19 | internal static readonly string _prefix = $"[{nameof(Logger)}]"; 20 | 21 | internal static readonly object s_filelocker = new object(); 22 | 23 | internal static readonly Queue s_queue = new Queue(); 24 | 25 | internal static TraceListener s_listener = null; 26 | 27 | internal static StreamWriter s_writer = null; 28 | 29 | internal static CancellationTokenSource s_cancel = null; 30 | 31 | internal static Task s_task = null; 32 | 33 | public static void Run(string path) 34 | { 35 | lock (s_filelocker) 36 | { 37 | if (s_listener is null) 38 | _ = Trace.Listeners.Add(s_listener = new LogListener()); 39 | 40 | s_writer?.Dispose(); 41 | var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read); 42 | s_writer = new StreamWriter(stream, Encoding.UTF8, 1024, false); 43 | 44 | if (s_cancel is null && s_task is null) 45 | { 46 | var cancel = new CancellationTokenSource(); 47 | s_task = Task.Run(new Func(() => _Monitor(cancel.Token))); 48 | s_cancel = cancel; 49 | } 50 | } 51 | } 52 | 53 | public static void Close() 54 | { 55 | lock (s_filelocker) 56 | { 57 | var tsk = s_task; 58 | var src = s_cancel; 59 | var wtr = s_writer; 60 | 61 | if (tsk is null || src is null || wtr is null) 62 | return; 63 | 64 | src.Cancel(); 65 | tsk.Wait(); 66 | src.Dispose(); 67 | wtr.Dispose(); 68 | 69 | s_task = null; 70 | s_cancel = null; 71 | s_writer = null; 72 | } 73 | } 74 | 75 | private static async Task _Monitor(CancellationToken token) 76 | { 77 | while (true) 78 | { 79 | var arr = default(string[]); 80 | lock (s_queue) 81 | { 82 | arr = s_queue.ToArray(); 83 | s_queue.Clear(); 84 | } 85 | 86 | if (arr.Length < 1) 87 | { 88 | if (token.IsCancellationRequested) 89 | return; 90 | await Task.Delay(4); 91 | continue; 92 | } 93 | 94 | try 95 | { 96 | lock (s_filelocker) 97 | { 98 | var wtr = s_writer; 99 | foreach (var i in arr) 100 | wtr.Write(i); 101 | wtr.Flush(); 102 | } 103 | } 104 | catch (Exception ex) 105 | { 106 | _InternalError(ex.ToString()); 107 | } 108 | } 109 | } 110 | 111 | private static void _Enqueue(string msg) 112 | { 113 | lock (s_queue) 114 | { 115 | var len = s_queue.Count + 1; 116 | var sub = len - _MaxQueueLength; 117 | if (sub > 0) 118 | { 119 | for (var i = 0; i < len; i++) 120 | _ = s_queue.Dequeue(); 121 | _InternalError("Log queue full!"); 122 | } 123 | s_queue.Enqueue(msg); 124 | } 125 | } 126 | 127 | /// 128 | /// 记录异常 (如果异常为空则不记录) 129 | /// 130 | public static void Error(Exception err, [CallerMemberName] string name = null, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0) 131 | { 132 | if (err == null) 133 | return; 134 | while (err is AggregateException a && a.InnerExceptions?.Count == 1 && a.InnerException is Exception val) 135 | err = val; 136 | Info(err.ToString(), name, file, line); 137 | } 138 | 139 | /// 140 | /// 记录自定义消息 (如果异常为空则不记录) 141 | /// 142 | public static void Info(string message, [CallerMemberName] string name = null, [CallerFilePath] string file = null, [CallerLineNumber] int line = 0) 143 | { 144 | if (message == null) 145 | return; 146 | var lbr = Environment.NewLine; 147 | 148 | var msg = $"[时间: {DateTime.Now:u}]" + lbr + 149 | $"[文件: {file}]" + lbr + 150 | $"[行号: {line}]" + lbr + 151 | $"[方法: {name}]" + lbr + 152 | $"{message}" + lbr + lbr; 153 | 154 | Trace.Write(_prefix + Environment.NewLine + msg); 155 | _Enqueue(msg); 156 | } 157 | 158 | internal static void _InternalError(string msg) 159 | { 160 | Trace.WriteLine(_prefix + Environment.NewLine + msg); 161 | } 162 | 163 | internal static void _Trace(string txt) 164 | { 165 | if (string.IsNullOrEmpty(txt) || txt.StartsWith(_prefix)) 166 | return; 167 | 168 | var lbr = Environment.NewLine; 169 | var msg = $"[时间: {DateTime.Now:u}]" + lbr + 170 | $"[来源: {nameof(Trace)}]" + lbr + 171 | $"{txt}" + lbr + lbr; 172 | _Enqueue(msg); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /code/Logger/LogListener.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Mikodev.Logger 4 | { 5 | internal class LogListener : TraceListener 6 | { 7 | public override void Write(string message) => Log._Trace(message); 8 | 9 | public override void WriteLine(string message) => Log._Trace(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /code/Logger/Logger.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Mikodev.Logger 6 | Mikodev.Network 7 | Messenger Project 8 | Mikodev 9 | Mikodev 2020 10 | 1.9.0 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /code/Messenger.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2005 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Messenger", "Messenger\Messenger.csproj", "{138B6748-7828-4442-A574-EBCF632A44B0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{875067CE-5717-451E-AEDB-22FDF3AD3C03}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Links", "Links\Links.csproj", "{A3ECBE4A-2D6A-490D-B8B5-6768A1253213}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Logger\Logger.csproj", "{F9128A2A-36B1-41BB-AE3C-6D2E93F66605}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {138B6748-7828-4442-A574-EBCF632A44B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {138B6748-7828-4442-A574-EBCF632A44B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {138B6748-7828-4442-A574-EBCF632A44B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {138B6748-7828-4442-A574-EBCF632A44B0}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {875067CE-5717-451E-AEDB-22FDF3AD3C03}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {A3ECBE4A-2D6A-490D-B8B5-6768A1253213}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {F9128A2A-36B1-41BB-AE3C-6D2E93F66605}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {F1C69CC5-C87B-412B-97C2-53F52C9652AC} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /code/Messenger/App.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/code/Messenger/App.ico -------------------------------------------------------------------------------- /code/Messenger/App.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /code/Messenger/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /code/Messenger/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Mikodev.Logger; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Input; 6 | 7 | namespace Messenger 8 | { 9 | public partial class App : Application 10 | { 11 | public event EventHandler TextBoxKeyDown; 12 | 13 | protected override void OnStartup(StartupEventArgs e) 14 | { 15 | base.OnStartup(e); 16 | 17 | DispatcherUnhandledException += (s, arg) => 18 | { 19 | arg.Handled = true; 20 | MessageBox.Show(arg.Exception.ToString(), "Unhandled Exception", MessageBoxButton.OK, MessageBoxImage.Error); 21 | Shutdown(1); 22 | }; 23 | 24 | void _Close(object sender, EventArgs args) 25 | { 26 | Framework.Close(); 27 | Log.Close(); 28 | } 29 | 30 | Exit += _Close; 31 | SessionEnding += _Close; 32 | 33 | Log.Run(nameof(Messenger) + ".log"); 34 | EventManager.RegisterClassHandler(typeof(TextBox), UIElement.KeyDownEvent, new KeyEventHandler((s, arg) => TextBoxKeyDown?.Invoke(s, arg))); 35 | Framework.Start(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/Messenger/Chatter.xaml.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Extensions; 2 | using Messenger.Models; 3 | using Messenger.Modules; 4 | using Microsoft.Win32; 5 | using Mikodev.Logger; 6 | using Mikodev.Network; 7 | using System; 8 | using System.ComponentModel; 9 | using System.IO; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Input; 13 | 14 | namespace Messenger 15 | { 16 | /// 17 | /// Interaction logic for Chatter.xaml 18 | /// 19 | public partial class Chatter : Page 20 | { 21 | private Profile _profile = null; 22 | 23 | private BindingList _messages = null; 24 | 25 | public Profile Profile => _profile; 26 | 27 | public Chatter() 28 | { 29 | InitializeComponent(); 30 | Loaded += _Loaded; 31 | Unloaded += _Unloaded; 32 | } 33 | 34 | private void _Loaded(object sender, RoutedEventArgs e) 35 | { 36 | HistoryModule.Receive += _HistoryReceiving; 37 | (Application.Current as App).TextBoxKeyDown += _TextBoxKeyDown; 38 | 39 | _profile = ProfileModule.Inscope; 40 | _messages = _profile.GetMessages(); 41 | uiProfileGrid.DataContext = _profile; 42 | uiMessageBox.ItemsSource = _messages; 43 | _messages.ListChanged += _ListChanged; 44 | uiMessageBox.ScrollIntoLastEx(); 45 | } 46 | 47 | private void _Unloaded(object sender, RoutedEventArgs e) 48 | { 49 | uiMessageBox.ItemsSource = null; 50 | HistoryModule.Receive -= _HistoryReceiving; 51 | (Application.Current as App).TextBoxKeyDown -= _TextBoxKeyDown; 52 | _messages.ListChanged -= _ListChanged; 53 | } 54 | 55 | private void _ListChanged(object sender, ListChangedEventArgs e) 56 | { 57 | if (e.ListChangedType != ListChangedType.ItemAdded) 58 | return; 59 | uiMessageBox.ScrollIntoLastEx(); 60 | } 61 | 62 | /// 63 | /// 拦截消息通知 64 | /// 65 | private void _HistoryReceiving(object sender, LinkEventArgs e) 66 | { 67 | if (e.Object.Index != _profile.Id) 68 | return; 69 | e.Finish = true; 70 | } 71 | 72 | private void _TextBoxKeyDown(object sender, KeyEventArgs e) 73 | { 74 | if (sender != uiInputBox || e.Key != Key.Enter) 75 | return; 76 | var mod = e.KeyboardDevice.Modifiers; 77 | var ins = SettingModule.Instance; 78 | if (ins.UseControlEnter && mod == ModifierKeys.Control || ins.UseEnter && mod == ModifierKeys.None) 79 | _SendText(); 80 | else 81 | uiInputBox.InsertEx(Environment.NewLine); 82 | e.Handled = true; 83 | } 84 | 85 | private void _Click(object sender, RoutedEventArgs e) 86 | { 87 | var tag = (e.OriginalSource as Button)?.Tag as string; 88 | if (tag == null) 89 | return; 90 | if (tag == "text") 91 | _SendText(); 92 | else if (tag == "image") 93 | _PushImage(); 94 | else if (tag == "clean") 95 | HistoryModule.Clear(_profile.Id); 96 | _ = uiInputBox.Focus(); 97 | } 98 | 99 | private void _SendText() 100 | { 101 | var str = uiInputBox.Text.TrimEnd(new char[] { '\0', '\r', '\n', '\t', ' ' }); 102 | if (str.Length < 1) 103 | return; 104 | uiInputBox.Text = string.Empty; 105 | PostModule.Text(_profile.Id, str); 106 | ProfileModule.SetRecent(_profile); 107 | } 108 | 109 | private void _PushImage() 110 | { 111 | var ofd = new OpenFileDialog() { Filter = "位图文件|*.bmp;*.png;*.jpg" }; 112 | if (ofd.ShowDialog() != true) 113 | return; 114 | try 115 | { 116 | var buf = CacheModule.ImageZoom(ofd.FileName); 117 | PostModule.Image(_profile.Id, buf); 118 | ProfileModule.SetRecent(_profile); 119 | } 120 | catch (Exception ex) 121 | { 122 | Log.Error(ex); 123 | Entrance.ShowError("发送图片失败", ex); 124 | } 125 | } 126 | 127 | private void _Share(string path) 128 | { 129 | if (File.Exists(path)) 130 | PostModule.File(_profile.Id, path); 131 | else if (Directory.Exists(path)) 132 | PostModule.Directory(_profile.Id, path); 133 | return; 134 | } 135 | 136 | private void _TextBoxPreviewDragOver(object sender, DragEventArgs e) 137 | { 138 | if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) 139 | e.Effects = DragDropEffects.None; 140 | else 141 | e.Effects = DragDropEffects.Copy; 142 | e.Handled = true; 143 | } 144 | 145 | private void _TextBoxPreviewDrop(object sender, DragEventArgs e) 146 | { 147 | if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) 148 | return; 149 | var arr = e.Data.GetData(DataFormats.FileDrop) as string[]; 150 | if (arr == null || arr.Length < 1) 151 | return; 152 | var val = arr[0]; 153 | _Share(val); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /code/Messenger/Commands.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Models; 2 | using Messenger.Modules; 3 | using Mikodev.Logger; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Windows; 7 | using System.Windows.Input; 8 | 9 | namespace Messenger 10 | { 11 | internal static class Commands 12 | { 13 | public static RoutedUICommand CopyText { get; } = new RoutedUICommand() { Text = "复制消息内容" }; 14 | 15 | public static RoutedUICommand Remove { get; } = new RoutedUICommand() { Text = "移除这条消息" }; 16 | 17 | public static RoutedUICommand ViewImage { get; } = new RoutedUICommand() { Text = "在图片查看器中查看" }; 18 | 19 | static Commands() 20 | { 21 | var cpy = new CommandBinding { Command = CopyText }; 22 | cpy.CanExecute += _CopyCanExecute; 23 | cpy.Executed += _CopyExecuted; 24 | 25 | var rmv = new CommandBinding { Command = Remove }; 26 | rmv.CanExecute += _RemoveCanExecute; 27 | rmv.Executed += _RemoveExecuted; 28 | 29 | var vie = new CommandBinding { Command = ViewImage }; 30 | vie.CanExecute += _ViewImageCanExecute; 31 | vie.Executed += _ViewImageExecuted; 32 | 33 | var list = Application.Current.MainWindow.CommandBindings; 34 | _ = list.Add(cpy); 35 | _ = list.Add(rmv); 36 | _ = list.Add(vie); 37 | } 38 | 39 | private static void _ViewImageCanExecute(object sender, CanExecuteRoutedEventArgs e) 40 | { 41 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 42 | if (msg is null || msg.Path != "image") 43 | e.CanExecute = false; 44 | else 45 | e.CanExecute = true; 46 | e.Handled = true; 47 | } 48 | 49 | private static void _ViewImageExecuted(object sender, ExecutedRoutedEventArgs e) 50 | { 51 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 52 | if (msg is null) 53 | return; 54 | var str = msg.Object as string; 55 | if (str == null) 56 | return; 57 | try 58 | { 59 | var flp = CacheModule.GetPath(str); 60 | _ = Process.Start(flp); 61 | } 62 | catch (Exception ex) 63 | { 64 | Log.Error(ex); 65 | } 66 | } 67 | 68 | private static void _RemoveCanExecute(object sender, CanExecuteRoutedEventArgs e) 69 | { 70 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 71 | if (msg is null) 72 | e.CanExecute = false; 73 | else 74 | e.CanExecute = true; 75 | e.Handled = true; 76 | } 77 | 78 | private static void _RemoveExecuted(object sender, ExecutedRoutedEventArgs e) 79 | { 80 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 81 | if (msg is null) 82 | return; 83 | HistoryModule.Remove(msg); 84 | e.Handled = true; 85 | } 86 | 87 | private static void _CopyCanExecute(object sender, CanExecuteRoutedEventArgs e) 88 | { 89 | var val = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 90 | if (val == null || val.Path != "text") 91 | e.CanExecute = false; 92 | else 93 | e.CanExecute = true; 94 | e.Handled = true; 95 | } 96 | 97 | private static void _CopyExecuted(object sender, ExecutedRoutedEventArgs e) 98 | { 99 | var msg = (e.OriginalSource as FrameworkElement)?.DataContext as Packet; 100 | if (msg?.MessageText is null) 101 | return; 102 | try 103 | { 104 | Clipboard.SetText(msg.MessageText); 105 | } 106 | catch (Exception ex) 107 | { 108 | Log.Error(ex); 109 | Entrance.ShowError("复制消息出错", ex); 110 | } 111 | e.Handled = true; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /code/Messenger/Connection.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 连接 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 107 | 108 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /code/Messenger/Connection.xaml.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Models; 2 | using Messenger.Modules; 3 | using Mikodev.Logger; 4 | using System; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Sockets; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Navigation; 13 | 14 | namespace Messenger 15 | { 16 | /// 17 | /// Interaction logic for Connection.xaml 18 | /// 19 | public partial class Connection : Page 20 | { 21 | private class _Temp 22 | { 23 | public string I { get; set; } = string.Empty; 24 | 25 | public string P { get; set; } = string.Empty; 26 | } 27 | 28 | private readonly BindingList _hosts = new BindingList(); 29 | 30 | public Connection() 31 | { 32 | InitializeComponent(); 33 | uiTableGrid.DataContext = new _Temp(); 34 | Loaded += _Loaded; 35 | } 36 | 37 | private void _Loaded(object sender, RoutedEventArgs e) 38 | { 39 | if (HostModule.Name != null) 40 | { 41 | uiHostBox.Text = HostModule.Name; 42 | uiPortBox.Text = HostModule.Port.ToString(); 43 | } 44 | uiIdBox.Text = ProfileModule.Id.ToString(); 45 | uiServerList.ItemsSource = _hosts; 46 | uiServerList.SelectionChanged += _SelectionChanged; 47 | } 48 | 49 | private void _SelectionChanged(object sender, SelectionChangedEventArgs e) 50 | { 51 | if (e.AddedItems.Count < 1) 52 | return; 53 | var itm = e.AddedItems[0] as Host; 54 | if (itm == null) 55 | return; 56 | uiHostBox.Text = itm.Address?.ToString(); 57 | uiPortBox.Text = itm.Port.ToString(); 58 | uiServerList.SelectedIndex = -1; 59 | } 60 | 61 | private async void _Click(object sender, RoutedEventArgs e) 62 | { 63 | async void _Refresh() 64 | { 65 | uiRefreshButton.IsEnabled = false; 66 | var lst = await Task.Run(HostModule.Refresh); 67 | foreach (var inf in lst) 68 | { 69 | var idx = _hosts.IndexOf(inf); 70 | if (idx < 0) 71 | _hosts.Add(inf); 72 | else _hosts[idx] = inf; 73 | } 74 | uiRefreshButton.IsEnabled = true; 75 | } 76 | 77 | var src = (Button)e.OriginalSource; 78 | if (src == uiBrowserButton) 79 | { 80 | uiBrowserButton.Visibility = Visibility.Collapsed; 81 | uiRefreshButton.Visibility = 82 | uiListGrid.Visibility = Visibility.Visible; 83 | _Refresh(); 84 | return; 85 | } 86 | else if (src == uiRefreshButton) 87 | { 88 | _hosts.Clear(); 89 | _Refresh(); 90 | return; 91 | } 92 | else if (src == uiConnectButton) 93 | { 94 | uiConnectButton.IsEnabled = false; 95 | try 96 | { 97 | var uid = int.Parse(uiIdBox.Text); 98 | var pot = int.Parse(uiPortBox.Text); 99 | var hos = uiHostBox.Text; 100 | 101 | var add = IPAddress.TryParse(hos, out var hst); 102 | if (add == false) 103 | hst = Dns.GetHostEntry(hos).AddressList.First(r => r.AddressFamily == AddressFamily.InterNetwork); 104 | var iep = new IPEndPoint(hst, pot); 105 | 106 | // 放弃等待该方法返回的任务 107 | _ = await LinkModule.Start(uid, iep); 108 | HostModule.Name = hos; 109 | HostModule.Port = pot; 110 | 111 | _ = NavigationService.Navigate(new PageFrame()); 112 | } 113 | catch (Exception ex) 114 | { 115 | Log.Error(ex); 116 | Entrance.ShowError("连接失败", ex); 117 | } 118 | uiConnectButton.IsEnabled = true; 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /code/Messenger/ControlProfileImage.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /code/Messenger/ControlProfileImage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | 3 | namespace Messenger 4 | { 5 | /// 6 | /// ControlProfileImage.xaml 的交互逻辑 7 | /// 8 | public partial class ControlProfileImage : UserControl 9 | { 10 | public ControlProfileImage() 11 | { 12 | InitializeComponent(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /code/Messenger/ControlShare.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /code/Messenger/PageGroups.xaml.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Modules; 2 | using Mikodev.Network; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | 6 | namespace Messenger 7 | { 8 | /// 9 | /// PageGroups.xaml 的交互逻辑 10 | /// 11 | public partial class PageGroups : Page 12 | { 13 | public PageGroups() 14 | { 15 | InitializeComponent(); 16 | Loaded += _Loaded; 17 | } 18 | 19 | private void _Loaded(object sender, RoutedEventArgs e) 20 | { 21 | PageManager.SetProfilePage(this, uiListbox, ProfileModule.GroupList); 22 | } 23 | 24 | private void _Click(object sender, RoutedEventArgs e) 25 | { 26 | var tag = (e.OriginalSource as Button)?.Tag as string; 27 | if (tag == null) 28 | return; 29 | if (tag == "edit") 30 | { 31 | var vis = uiEditGrid.Visibility; 32 | uiEditGrid.Visibility = (vis == Visibility.Visible) ? Visibility.Collapsed : Visibility.Visible; 33 | } 34 | else if (tag == "apply") 35 | { 36 | if (string.Equals(uiEditBox.Text, ProfileModule.GroupLabels) == false && ProfileModule.SetGroupLabels(uiEditBox.Text) == false) 37 | Entrance.ShowError($"最多允许 {Links.GroupLabelLimit} 个群组标签", null); 38 | else 39 | uiEditGrid.Visibility = Visibility.Collapsed; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /code/Messenger/PageManager.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Models; 2 | using Messenger.Modules; 3 | using System.ComponentModel; 4 | using System.Windows.Controls; 5 | 6 | namespace Messenger 7 | { 8 | internal static class PageManager 9 | { 10 | public static void _ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e) 11 | { 12 | var lst = e.AddedItems; 13 | if (lst.Count < 1) 14 | return; 15 | var itm = lst[0] as Profile; 16 | ProfileModule.SetInscope(itm); 17 | } 18 | 19 | public static void SetSelectedProfile(ListBox listbox, BindingList list) 20 | { 21 | var idx = list.IndexOf(ProfileModule.Inscope); 22 | listbox.SelectedIndex = idx; 23 | } 24 | 25 | public static void SetProfilePage(Page page, ListBox listbox, BindingList list) 26 | { 27 | SetSelectedProfile(listbox, list); 28 | 29 | var hdr = new ListChangedEventHandler((s, e) => 30 | { 31 | if (e.ListChangedType != ListChangedType.ItemAdded) 32 | return; 33 | SetSelectedProfile(listbox, list); 34 | }); 35 | 36 | list.ListChanged += hdr; 37 | listbox.SelectionChanged += _ListBoxSelectionChanged; 38 | 39 | page.Unloaded += delegate 40 | { 41 | list.ListChanged -= hdr; 42 | listbox.SelectionChanged -= _ListBoxSelectionChanged; 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/Messenger/PageOption.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 17 | 设置 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /code/Messenger/PageRecent.xaml.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Modules; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | 5 | namespace Messenger 6 | { 7 | /// 8 | /// PageRecent.xaml 的交互逻辑 9 | /// 10 | public partial class PageRecent : Page 11 | { 12 | public PageRecent() 13 | { 14 | InitializeComponent(); 15 | Loaded += _Loaded; 16 | } 17 | 18 | private void _Loaded(object sender, RoutedEventArgs e) 19 | { 20 | PageManager.SetProfilePage(this, uiListbox, ProfileModule.RecentList); 21 | } 22 | 23 | private void _Click(object sender, RoutedEventArgs e) 24 | { 25 | var tag = (e.OriginalSource as Button)?.Tag as string; 26 | if (tag == null) 27 | return; 28 | if (tag == "clear") 29 | { 30 | ProfileModule.RecentList.Clear(); 31 | return; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /code/Messenger/PageShare.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 50 | 51 | 53 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /code/Messenger/Shower.xaml.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Modules; 2 | using Mikodev.Logger; 3 | using System; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | 7 | namespace Messenger 8 | { 9 | /// 10 | /// Interaction logic for Shower.xaml 11 | /// 12 | public partial class Shower : Page 13 | { 14 | public Shower() 15 | { 16 | InitializeComponent(); 17 | } 18 | 19 | private void _Click(object sender, RoutedEventArgs e) 20 | { 21 | var tag = (e.OriginalSource as Button)?.Tag as string; 22 | if (tag == null) 23 | return; 24 | if (tag == "apply") 25 | { 26 | ProfileModule.SetProfile(uiNameBox.Text, uiSignBox.Text); 27 | } 28 | else if (tag == "image") 29 | { 30 | var ofd = new System.Windows.Forms.OpenFileDialog() { Filter = "位图文件|*.bmp;*.png;*.jpg" }; 31 | if (ofd.ShowDialog() != System.Windows.Forms.DialogResult.OK) 32 | return; 33 | try 34 | { 35 | ProfileModule.SetImage(ofd.FileName); 36 | } 37 | catch (Exception ex) 38 | { 39 | Entrance.ShowError("设置头像失败!", ex); 40 | Log.Error(ex); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /code/Messenger/Tools/ImageSourceConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | using System.Windows.Media; 5 | using System.Windows.Media.Imaging; 6 | 7 | namespace Messenger.Tools 8 | { 9 | internal class ImageSourceConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | var str = value as string; 14 | if (string.IsNullOrEmpty(str)) 15 | return null; 16 | return new ImageBrush(new BitmapImage(new Uri(str))); 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /code/Messenger/Tools/LengthUnitConverter.cs: -------------------------------------------------------------------------------- 1 | using Messenger.Extensions; 2 | using System; 3 | using System.Globalization; 4 | using System.Windows.Data; 5 | 6 | namespace Messenger.Tools 7 | { 8 | /// 9 | /// 将大小转化为带单位的字符串 10 | /// 11 | internal class LengthUnitConverter : IValueConverter 12 | { 13 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => Extension.ToUnitEx(System.Convert.ToInt64(value)); 14 | 15 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/Messenger/Tools/LogicToPixelConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows; 4 | using System.Windows.Data; 5 | using System.Windows.Media; 6 | 7 | namespace Messenger.Tools 8 | { 9 | internal class LogicToPixelConverter : IValueConverter 10 | { 11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 12 | { 13 | // 将像素尺寸转换到逻辑尺寸 (用于在高 DPI 环境以像素为单位进行操作) 14 | if (value is Visual vis && targetType == typeof(Thickness) && parameter is Thickness mar) 15 | { 16 | var win = PresentationSource.FromVisual(vis); 17 | var hor = 1.0 / win.CompositionTarget.TransformToDevice.M11; 18 | var ver = 1.0 / win.CompositionTarget.TransformToDevice.M22; 19 | var thi = new Thickness(hor * mar.Left, ver * mar.Top, hor * mar.Right, ver * mar.Bottom); 20 | return thi; 21 | } 22 | return null; 23 | } 24 | 25 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /code/Messenger/Tools/ObjectTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace Messenger.Tools 6 | { 7 | internal class ObjectTypeConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value?.GetType(); 10 | 11 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /code/Messenger/Tools/ProfileHintConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace Messenger.Tools 6 | { 7 | /// 8 | /// 当未读消息过多时 标注 "+" 号 9 | /// 10 | internal class ProfileHintConverter : IValueConverter 11 | { 12 | public int MaxShowValue { get; set; } = 9; 13 | 14 | public string OverflowText { get; set; } = "9+"; 15 | 16 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 17 | { 18 | switch (value) 19 | { 20 | case int val when val > MaxShowValue: 21 | return OverflowText; 22 | 23 | case int val when val < 0 == false: 24 | return val.ToString(); 25 | 26 | default: 27 | return 0.ToString(); 28 | } 29 | } 30 | 31 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /code/Messenger/Tools/ProfileIdValidation.cs: -------------------------------------------------------------------------------- 1 | using Mikodev.Network; 2 | using System.Globalization; 3 | using System.Windows.Controls; 4 | 5 | namespace Messenger.Tools 6 | { 7 | internal class ProfileIdValidation : ValidationRule 8 | { 9 | public override ValidationResult Validate(object value, CultureInfo cultureInfo) 10 | { 11 | var str = value as string; 12 | if (string.IsNullOrEmpty(str)) 13 | return new ValidationResult(false, "输入为空"); 14 | if (int.TryParse(str, out var id)) 15 | if (Links.Id < id && id < Links.DefaultId) 16 | return new ValidationResult(true, string.Empty); 17 | else 18 | return new ValidationResult(false, $"编号应大于 {Links.Id} 且小于 {Links.DefaultId}"); 19 | return new ValidationResult(false, "输入无效"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/Messenger/Tools/ProfileLogoConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.RegularExpressions; 4 | using System.Windows.Data; 5 | 6 | namespace Messenger.Tools 7 | { 8 | /// 9 | /// 为没有头像的用户生成字符 Logo 10 | /// 11 | internal class ProfileLogoConverter : IValueConverter 12 | { 13 | private const int _limit = 3; 14 | 15 | private const int _short = 2; 16 | 17 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 18 | { 19 | var reg = new Regex(@"^[A-Za-z0-9]+$"); 20 | var str = (value == null) ? string.Empty : value.ToString(); 21 | if (str.Length > _limit && _limit > _short && _short > 0) 22 | str = str.Substring(0, _short); 23 | if (str.Length > 1 && reg.IsMatch(str) == false) 24 | str = str.Substring(0, 1); 25 | return str.ToUpper(); 26 | } 27 | 28 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /code/Messenger/Tools/SocketPortValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Net; 3 | using System.Windows.Controls; 4 | 5 | namespace Messenger.Tools 6 | { 7 | internal class SocketPortValidation : ValidationRule 8 | { 9 | public override ValidationResult Validate(object value, CultureInfo cultureInfo) 10 | { 11 | var str = value as string; 12 | if (string.IsNullOrEmpty(str)) 13 | return new ValidationResult(false, "输入为空"); 14 | if (int.TryParse(str, out var val)) 15 | if (val >= IPEndPoint.MinPort && val <= IPEndPoint.MaxPort) 16 | return new ValidationResult(true, string.Empty); 17 | else 18 | return new ValidationResult(false, $"端口号应当介于 {IPEndPoint.MinPort} 和 {IPEndPoint.MaxPort} 之间"); 19 | return new ValidationResult(false, "输入无效"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/Messenger/Tools/StringEmptyConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Windows.Data; 4 | 5 | namespace Messenger.Tools 6 | { 7 | internal class StringEmptyConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => string.IsNullOrEmpty(value?.ToString()); 10 | 11 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new InvalidOperationException(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /image/00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/00.png -------------------------------------------------------------------------------- /image/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/01.png -------------------------------------------------------------------------------- /image/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/02.png -------------------------------------------------------------------------------- /image/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afxres/messenger/f7d60699b0f8c646c7a0a49d01db04f276f690e1/image/03.png --------------------------------------------------------------------------------