diff --git a/Signal-Windows.sln b/Signal-Windows.sln index 5e1310f..2d70456 100644 --- a/Signal-Windows.sln +++ b/Signal-Windows.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +VisualStudioVersion = 15.0.27004.2002 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows", "Signal-Windows\Signal-Windows.csproj", "{41736A64-5B66-44AF-879A-501192A46920}" EndProject diff --git a/Signal-Windows/Controls/AddContactListElement.xaml b/Signal-Windows/Controls/AddContactListElement.xaml new file mode 100644 index 0000000..0968212 --- /dev/null +++ b/Signal-Windows/Controls/AddContactListElement.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Signal-Windows/Controls/AddContactListElement.xaml.cs b/Signal-Windows/Controls/AddContactListElement.xaml.cs new file mode 100644 index 0000000..7e75722 --- /dev/null +++ b/Signal-Windows/Controls/AddContactListElement.xaml.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Signal_Windows.Models; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Signal_Windows.Controls +{ + public sealed partial class AddContactListElement : UserControl, INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + public AddContactListElement() + { + this.InitializeComponent(); + this.DataContextChanged += AddContactListElement_DataContextChanged; + } + + public string _DisplayName; + public string DisplayName + { + get { return _DisplayName; } + set + { + _DisplayName = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DisplayName))); + } + } + + public string _PhoneNumber; + public string PhoneNumber + { + get { return _PhoneNumber; } + set + { + _PhoneNumber = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PhoneNumber))); + } + } + + public ImageSource _ContactPhoto = null; + public ImageSource ContactPhoto + { + get { return _ContactPhoto; } + set + { + _ContactPhoto = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContactPhoto))); + } + } + + public bool _OnSignal; + public bool OnSignal + { + get { return _OnSignal; } + set + { + _OnSignal = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OnSignal))); + } + } + + public PhoneContact Model + { + get + { + return DataContext as PhoneContact; + } + set + { + DataContext = value; + } + } + + private void AddContactListElement_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if (Model != null) + { + Model.View = this; + DisplayName = Model.Name; + PhoneNumber = Model.PhoneNumber; + ContactPhoto = Model.Photo; + OnSignal = Model.OnSignal; + } + } + } +} diff --git a/Signal-Windows/Controls/VirtualizedMessagesCollection.cs b/Signal-Windows/Controls/VirtualizedMessagesCollection.cs index d659ceb..17909b7 100644 --- a/Signal-Windows/Controls/VirtualizedMessagesCollection.cs +++ b/Signal-Windows/Controls/VirtualizedMessagesCollection.cs @@ -1,230 +1,230 @@ - -using Signal_Windows.Models; -using Signal_Windows.Storage; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Signal_Windows.Controls -{ - public class SignalMessageContainer - { - public SignalMessage Message; - public int Index; - public SignalMessageContainer(SignalMessage message, int index) - { - Message = message; - Index = index; - } - } - - public class SignalUnreadMarker - { - public string Text = ""; - } - - public class VirtualizedCollection : IList, INotifyCollectionChanged - { - private const int PAGE_SIZE = 50; - public event NotifyCollectionChangedEventHandler CollectionChanged; - private Dictionary> Cache = new Dictionary>(); - private SignalConversation Conversation; - private SignalUnreadMarker UnreadMarker = new SignalUnreadMarker(); - public int UnreadMarkerIndex = -1; - - public VirtualizedCollection(SignalConversation c) - { - Conversation = c; - if (Conversation.LastSeenMessageIndex > 0 && Conversation.LastSeenMessageIndex < Conversation.MessagesCount ) - { - UnreadMarkerIndex = (int) Conversation.LastSeenMessageIndex; - UnreadMarker.Text = Conversation.UnreadCount > 1 ? $"{Conversation.UnreadCount} new messages" : "1 new message"; - } - else - { - UnreadMarkerIndex = -1; - } - } - - private static int GetPageIndex(int itemIndex) - { - return itemIndex / PAGE_SIZE; - } - - - public object this[int index] - { - get - { - if (UnreadMarkerIndex > 0) - { - if (index < UnreadMarkerIndex) - { - return Get(index); - } - else if (index == UnreadMarkerIndex) - { - return UnreadMarker; - } - else - { - return Get(index - 1); - } - } - else - { - return Get(index); - } - } - set => throw new NotImplementedException(); - } - - private SignalMessageContainer Get(int index) - { - int inpageIndex = index % PAGE_SIZE; - int pageIndex = GetPageIndex(index); - if (!Cache.ContainsKey(pageIndex)) - { - Debug.WriteLine($"cache miss {pageIndex}"); - Cache[pageIndex] = SignalDBContext.GetMessagesLocked(Conversation, pageIndex * PAGE_SIZE, PAGE_SIZE); - } - var page = Cache[pageIndex]; - var item = page[inpageIndex]; - return page[inpageIndex]; - } - - public bool IsFixedSize => false; - - public bool IsReadOnly => false; - - public int Count - { - get - { - if (UnreadMarkerIndex > 0) - { - return (int)Conversation.MessagesCount + 1; - } - else - { - return (int)Conversation.MessagesCount; - } - } - } - - public bool IsSynchronized => false; - - public object SyncRoot => this; - - /// - /// "Adds" a SignalMessageContainer to this virtualized collection. - /// - /// The message may (if incoming) or may not (if outgoing) already be present in the database, so we explicitly insert at the correct position in the cache line. - /// Count is mapped to the SignalConversation's MessagesCount, so callers must update appropriately before calling this method, and no async method must be called in between. - /// The object to add to the VirtualizedMessagesCollection. - /// The position into which the new element was inserted, or -1 to indicate that the item was not inserted into the collection. - public int Add(object value, bool forcedScroll) - { - if (forcedScroll && UnreadMarkerIndex > 0) - { - var old = UnreadMarkerIndex; - UnreadMarkerIndex = -1; - CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, UnreadMarker, old)); - } - var message = value as SignalMessageContainer; - int inpageIndex = message.Index % PAGE_SIZE; - int pageIndex = GetPageIndex(message.Index); - Debug.WriteLine($"VirtualizedCollection.Add Id={message.Message.Id} Index={message.Index} PageIndex={pageIndex} InpageIndex={inpageIndex} "); - if (!Cache.ContainsKey(pageIndex)) - { - Cache[pageIndex] = SignalDBContext.GetMessagesLocked(Conversation, pageIndex * PAGE_SIZE, PAGE_SIZE); - } - Cache[pageIndex].Insert(inpageIndex, message); - int virtualIndex = GetVirtualIndex(message.Index); - Debug.WriteLine($"NotifyCollectionChangedAction.Add index={message.Index} virtualIndex={virtualIndex}"); - CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, message, virtualIndex)); - return message.Index; - } - - public void Clear() - { - throw new NotImplementedException(); - } - - public bool Contains(object value) - { - throw new NotImplementedException(); - } - - public void CopyTo(Array array, int index) - { - throw new NotImplementedException(); - } - - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - public int IndexOf(object value) - { - if (value is SignalMessageContainer) - { - SignalMessageContainer smc = (SignalMessageContainer) value; - return GetVirtualIndex(smc.Index); - } - else if (value is SignalUnreadMarker) - { - return UnreadMarkerIndex; - } - else - { - return -1; - } - } - - internal int GetVirtualIndex(int rawIndex) - { - if (UnreadMarkerIndex > 0) - { - if (rawIndex < UnreadMarkerIndex) - { - return rawIndex; - } - else - { - return rawIndex + 1; - } - } - else - { - return rawIndex; - } - } - - public void Insert(int index, object value) - { - throw new NotImplementedException(); - } - - public void Remove(object value) - { - throw new NotImplementedException(); - } - - public void RemoveAt(int index) - { - throw new NotImplementedException(); - } - - public int Add(object value) - { - throw new NotImplementedException(); - } - } -} + +using Signal_Windows.Models; +using Signal_Windows.Storage; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Signal_Windows.Controls +{ + public class SignalMessageContainer + { + public SignalMessage Message; + public int Index; + public SignalMessageContainer(SignalMessage message, int index) + { + Message = message; + Index = index; + } + } + + public class SignalUnreadMarker + { + public string Text = ""; + } + + public class VirtualizedCollection : IList, INotifyCollectionChanged + { + private const int PAGE_SIZE = 50; + public event NotifyCollectionChangedEventHandler CollectionChanged; + private Dictionary> Cache = new Dictionary>(); + private SignalConversation Conversation; + private SignalUnreadMarker UnreadMarker = new SignalUnreadMarker(); + public int UnreadMarkerIndex = -1; + + public VirtualizedCollection(SignalConversation c) + { + Conversation = c; + if (Conversation.LastSeenMessageIndex > 0 && Conversation.LastSeenMessageIndex < Conversation.MessagesCount ) + { + UnreadMarkerIndex = (int) Conversation.LastSeenMessageIndex; + UnreadMarker.Text = Conversation.UnreadCount > 1 ? $"{Conversation.UnreadCount} new messages" : "1 new message"; + } + else + { + UnreadMarkerIndex = -1; + } + } + + private static int GetPageIndex(int itemIndex) + { + return itemIndex / PAGE_SIZE; + } + + + public object this[int index] + { + get + { + if (UnreadMarkerIndex > 0) + { + if (index < UnreadMarkerIndex) + { + return Get(index); + } + else if (index == UnreadMarkerIndex) + { + return UnreadMarker; + } + else + { + return Get(index - 1); + } + } + else + { + return Get(index); + } + } + set => throw new NotImplementedException(); + } + + private SignalMessageContainer Get(int index) + { + int inpageIndex = index % PAGE_SIZE; + int pageIndex = GetPageIndex(index); + if (!Cache.ContainsKey(pageIndex)) + { + Debug.WriteLine($"cache miss {pageIndex}"); + Cache[pageIndex] = SignalDBContext.GetMessagesLocked(Conversation, pageIndex * PAGE_SIZE, PAGE_SIZE); + } + var page = Cache[pageIndex]; + var item = page[inpageIndex]; + return page[inpageIndex]; + } + + public bool IsFixedSize => false; + + public bool IsReadOnly => false; + + public int Count + { + get + { + if (UnreadMarkerIndex > 0) + { + return (int)Conversation.MessagesCount + 1; + } + else + { + return (int)Conversation.MessagesCount; + } + } + } + + public bool IsSynchronized => false; + + public object SyncRoot => this; + + /// + /// "Adds" a SignalMessageContainer to this virtualized collection. + /// + /// The message may (if incoming) or may not (if outgoing) already be present in the database, so we explicitly insert at the correct position in the cache line. + /// Count is mapped to the SignalConversation's MessagesCount, so callers must update appropriately before calling this method, and no async method must be called in between. + /// The object to add to the VirtualizedMessagesCollection. + /// The position into which the new element was inserted, or -1 to indicate that the item was not inserted into the collection. + public int Add(object value, bool forcedScroll) + { + if (forcedScroll && UnreadMarkerIndex > 0) + { + var old = UnreadMarkerIndex; + UnreadMarkerIndex = -1; + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, UnreadMarker, old)); + } + var message = value as SignalMessageContainer; + int inpageIndex = message.Index % PAGE_SIZE; + int pageIndex = GetPageIndex(message.Index); + Debug.WriteLine($"VirtualizedCollection.Add Id={message.Message.Id} Index={message.Index} PageIndex={pageIndex} InpageIndex={inpageIndex} "); + if (!Cache.ContainsKey(pageIndex)) + { + Cache[pageIndex] = SignalDBContext.GetMessagesLocked(Conversation, pageIndex * PAGE_SIZE, PAGE_SIZE); + } + Cache[pageIndex].Insert(inpageIndex, message); + int virtualIndex = GetVirtualIndex(message.Index); + Debug.WriteLine($"NotifyCollectionChangedAction.Add index={message.Index} virtualIndex={virtualIndex}"); + CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, message, virtualIndex)); + return message.Index; + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(object value) + { + throw new NotImplementedException(); + } + + public void CopyTo(Array array, int index) + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public int IndexOf(object value) + { + if (value is SignalMessageContainer) + { + SignalMessageContainer smc = (SignalMessageContainer) value; + return GetVirtualIndex(smc.Index); + } + else if (value is SignalUnreadMarker) + { + return UnreadMarkerIndex; + } + else + { + return -1; + } + } + + internal int GetVirtualIndex(int rawIndex) + { + if (UnreadMarkerIndex > 0) + { + if (rawIndex < UnreadMarkerIndex) + { + return rawIndex; + } + else + { + return rawIndex + 1; + } + } + else + { + return rawIndex; + } + } + + public void Insert(int index, object value) + { + throw new NotImplementedException(); + } + + public void Remove(object value) + { + throw new NotImplementedException(); + } + + public void RemoveAt(int index) + { + throw new NotImplementedException(); + } + + public int Add(object value) + { + throw new NotImplementedException(); + } + } +} diff --git a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs b/Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs index db49348..2242a92 100644 --- a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs +++ b/Signal-Windows/Migrations/SignalDB/20170901124533_m3.Designer.cs @@ -1,252 +1,252 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Signal_Windows.Storage; -using Signal_Windows.Models; - -namespace Signal_Windows.Migrations -{ - [DbContext(typeof(SignalDBContext))] - [Migration("20170901124533_m3")] - partial class m3 - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2"); - - modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("ContactId"); - - b.Property("GroupId"); - - b.HasKey("Id"); - - b.HasIndex("ContactId"); - - b.HasIndex("GroupId"); - - b.ToTable("GroupMemberships"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("ContentType"); - - b.Property("FileName"); - - b.Property("Key"); - - b.Property("MessageId"); - - b.Property("Relay"); - - b.Property("SentFileName"); - - b.Property("Status"); - - b.Property("StorageId"); - - b.HasKey("Id"); - - b.HasIndex("MessageId"); - - b.ToTable("Attachments"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("AvatarFile"); - - b.Property("CanReceive"); - - b.Property("Discriminator") - .IsRequired(); - - b.Property("Draft"); - - b.Property("ExpiresInSeconds"); - - b.Property("LastActiveTimestamp"); - - b.Property("LastMessageId"); - - b.Property("LastSeenMessageId"); - - b.Property("LastSeenMessageIndex"); - - b.Property("MessagesCount"); - - b.Property("ThreadDisplayName"); - - b.Property("ThreadId"); - - b.Property("UnreadCount"); - - b.HasKey("Id"); - - b.HasIndex("LastMessageId"); - - b.HasIndex("LastSeenMessageId"); - - b.HasIndex("ThreadId"); - - b.ToTable("SignalConversation"); - - b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("DeviceId"); - - b.Property("Timestamp"); - - b.Property("Username"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("Timestamp"); - - b.HasIndex("Username"); - - b.ToTable("EarlyReceipts"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("AttachmentsCount"); - - b.Property("AuthorId"); - - b.Property("ComposedTimestamp"); - - b.Property("Contentrowid"); - - b.Property("DeviceId"); - - b.Property("Direction"); - - b.Property("ExpiresAt"); - - b.Property("Read"); - - b.Property("Receipts"); - - b.Property("ReceivedTimestamp"); - - b.Property("Status"); - - b.Property("ThreadId"); - - b.Property("Type"); - - b.HasKey("Id"); - - b.HasIndex("AuthorId"); - - b.HasIndex("Contentrowid"); - - b.HasIndex("ThreadId"); - - b.ToTable("Messages"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => - { - b.Property("rowid") - .ValueGeneratedOnAdd(); - - b.Property("Content"); - - b.HasKey("rowid"); - - b.ToTable("Messages_fts"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => - { - b.HasBaseType("Signal_Windows.Models.SignalConversation"); - - b.Property("Color"); - - b.ToTable("SignalContact"); - - b.HasDiscriminator().HasValue("SignalContact"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => - { - b.HasBaseType("Signal_Windows.Models.SignalConversation"); - - - b.ToTable("SignalGroup"); - - b.HasDiscriminator().HasValue("SignalGroup"); - }); - - modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => - { - b.HasOne("Signal_Windows.Models.SignalContact", "Contact") - .WithMany("GroupMemberships") - .HasForeignKey("ContactId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Signal_Windows.Models.SignalGroup", "Group") - .WithMany("GroupMemberships") - .HasForeignKey("GroupId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => - { - b.HasOne("Signal_Windows.Models.SignalMessage", "Message") - .WithMany("Attachments") - .HasForeignKey("MessageId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => - { - b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") - .WithMany() - .HasForeignKey("LastMessageId"); - - b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") - .WithMany() - .HasForeignKey("LastSeenMessageId"); - }); - - modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => - { - b.HasOne("Signal_Windows.Models.SignalContact", "Author") - .WithMany() - .HasForeignKey("AuthorId"); - - b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") - .WithMany() - .HasForeignKey("Contentrowid"); - }); - } - } -} +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Signal_Windows.Storage; +using Signal_Windows.Models; + +namespace Signal_Windows.Migrations +{ + [DbContext(typeof(SignalDBContext))] + [Migration("20170901124533_m3")] + partial class m3 + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2"); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContactId"); + + b.Property("GroupId"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("GroupId"); + + b.ToTable("GroupMemberships"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ContentType"); + + b.Property("FileName"); + + b.Property("Key"); + + b.Property("MessageId"); + + b.Property("Relay"); + + b.Property("SentFileName"); + + b.Property("Status"); + + b.Property("StorageId"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AvatarFile"); + + b.Property("CanReceive"); + + b.Property("Discriminator") + .IsRequired(); + + b.Property("Draft"); + + b.Property("ExpiresInSeconds"); + + b.Property("LastActiveTimestamp"); + + b.Property("LastMessageId"); + + b.Property("LastSeenMessageId"); + + b.Property("LastSeenMessageIndex"); + + b.Property("MessagesCount"); + + b.Property("ThreadDisplayName"); + + b.Property("ThreadId"); + + b.Property("UnreadCount"); + + b.HasKey("Id"); + + b.HasIndex("LastMessageId"); + + b.HasIndex("LastSeenMessageId"); + + b.HasIndex("ThreadId"); + + b.ToTable("SignalConversation"); + + b.HasDiscriminator("Discriminator").HasValue("SignalConversation"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalEarlyReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("DeviceId"); + + b.Property("Timestamp"); + + b.Property("Username"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("Username"); + + b.ToTable("EarlyReceipts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AttachmentsCount"); + + b.Property("AuthorId"); + + b.Property("ComposedTimestamp"); + + b.Property("Contentrowid"); + + b.Property("DeviceId"); + + b.Property("Direction"); + + b.Property("ExpiresAt"); + + b.Property("Read"); + + b.Property("Receipts"); + + b.Property("ReceivedTimestamp"); + + b.Property("Status"); + + b.Property("ThreadId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("Contentrowid"); + + b.HasIndex("ThreadId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessageContent", b => + { + b.Property("rowid") + .ValueGeneratedOnAdd(); + + b.Property("Content"); + + b.HasKey("rowid"); + + b.ToTable("Messages_fts"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalContact", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + b.Property("Color"); + + b.ToTable("SignalContact"); + + b.HasDiscriminator().HasValue("SignalContact"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalGroup", b => + { + b.HasBaseType("Signal_Windows.Models.SignalConversation"); + + + b.ToTable("SignalGroup"); + + b.HasDiscriminator().HasValue("SignalGroup"); + }); + + modelBuilder.Entity("Signal_Windows.Models.GroupMembership", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Contact") + .WithMany("GroupMemberships") + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Signal_Windows.Models.SignalGroup", "Group") + .WithMany("GroupMemberships") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalAttachment", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "Message") + .WithMany("Attachments") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalConversation", b => + { + b.HasOne("Signal_Windows.Models.SignalMessage", "LastMessage") + .WithMany() + .HasForeignKey("LastMessageId"); + + b.HasOne("Signal_Windows.Models.SignalMessage", "LastSeenMessage") + .WithMany() + .HasForeignKey("LastSeenMessageId"); + }); + + modelBuilder.Entity("Signal_Windows.Models.SignalMessage", b => + { + b.HasOne("Signal_Windows.Models.SignalContact", "Author") + .WithMany() + .HasForeignKey("AuthorId"); + + b.HasOne("Signal_Windows.Models.SignalMessageContent", "Content") + .WithMany() + .HasForeignKey("Contentrowid"); + }); + } + } +} diff --git a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs b/Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs index 5672155..0c5fa3c 100644 --- a/Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs +++ b/Signal-Windows/Migrations/SignalDB/20170901124533_m3.cs @@ -1,25 +1,25 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Signal_Windows.Migrations -{ - public partial class m3 : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "LastSeenMessageIndex", - table: "SignalConversation", - nullable: false, - defaultValue: 0L); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "LastSeenMessageIndex", - table: "SignalConversation"); - } - } -} +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Signal_Windows.Migrations +{ + public partial class m3 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastSeenMessageIndex", + table: "SignalConversation", + nullable: false, + defaultValue: 0L); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastSeenMessageIndex", + table: "SignalConversation"); + } + } +} diff --git a/Signal-Windows/Models/PhoneContact.cs b/Signal-Windows/Models/PhoneContact.cs new file mode 100644 index 0000000..209fd44 --- /dev/null +++ b/Signal-Windows/Models/PhoneContact.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Signal_Windows.Controls; +using Windows.UI.Xaml.Media; + +namespace Signal_Windows.Models +{ + public class PhoneContact + { + public string Id { get; set; } + public string Name { get; set; } + public string PhoneNumber { get; set; } + public ImageSource Photo { get; set; } + public bool OnSignal { get; set; } + public AddContactListElement View; + } +} diff --git a/Signal-Windows/Package.appxmanifest b/Signal-Windows/Package.appxmanifest index b898f84..a553b18 100644 --- a/Signal-Windows/Package.appxmanifest +++ b/Signal-Windows/Package.appxmanifest @@ -20,6 +20,18 @@ + + + + Signal Private Messenger Contact + + + + + Signal Private Messenger Message + + + diff --git a/Signal-Windows/Signal-Windows.csproj b/Signal-Windows/Signal-Windows.csproj index fa81960..d0cab39 100644 --- a/Signal-Windows/Signal-Windows.csproj +++ b/Signal-Windows/Signal-Windows.csproj @@ -94,7 +94,6 @@ - @@ -141,6 +140,9 @@ App.xaml + + AddContactListElement.xaml + IdentityKeyChangeMessage.xaml @@ -179,6 +181,7 @@ 20170901124533_m3.cs + @@ -247,6 +250,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile @@ -300,16 +307,53 @@ MSBuild:Compile - + + + 2.5.10.3 + + + 1.1.2 + + + 1.1.2 + + + 1.1.2 + + + 1.1.1 + + + 6.0.1 + + + 1.5.1 + + + 2.0.0 + + + 5.3.0 + + + 4.0.1 + + + 1.0.0 + + + 2.2.9 + + 14.0 - \ No newline at end of file diff --git a/Signal-Windows/Storage/DB.cs b/Signal-Windows/Storage/DB.cs index 7b2efd0..7aa3fd4 100644 --- a/Signal-Windows/Storage/DB.cs +++ b/Signal-Windows/Storage/DB.cs @@ -1130,7 +1130,7 @@ public static SignalContact GetOrCreateContactLocked(string username, long times ThreadDisplayName = username, CanReceive = true, LastActiveTimestamp = timestamp, - Color = Utils.Colors[Utils.CalculateDefaultColorIndex(username)] + Color = Utils.CalculateDefaultColor(username) }; ctx.Contacts.Add(contact); ctx.SaveChanges(); diff --git a/Signal-Windows/Utils.cs b/Signal-Windows/Utils.cs index 0b12799..acd8614 100644 --- a/Signal-Windows/Utils.cs +++ b/Signal-Windows/Utils.cs @@ -12,7 +12,7 @@ namespace Signal_Windows { - public class Utils + public static class Utils { public static string[] Colors = { "red", @@ -86,19 +86,35 @@ public static SolidColorBrush GetBrushFromColor(string signalcolor) } } - public static int CalculateDefaultColorIndex(string title) + public static void AddRange(this ObservableCollection observableCollection, IEnumerable collection) { - if (title.Length == 0) + foreach (var item in collection) { - return 0; + observableCollection.Add(item); } - var hash = 0; - for (int i = 0; i < title.Length; i++) + } + + public static string CalculateDefaultColor(string title) + { + return Colors[Math.Abs(JavaStringHashCode(title)) % Colors.Length]; + } + + public static SolidColorBrush GetDefaultColor(string title) + { + return GetBrushFromColor(CalculateDefaultColor(title)); + } + + public static int JavaStringHashCode(string str) + { + int h = 0; + if (str.Length > 0) { - hash = ((hash << 5) - hash) + title[i]; - hash = hash & hash; + for (int i = 0; i < str.Length; i++) + { + h = 31 * h + str[i]; + } } - return Math.Abs(hash) % 15; + return h; } public static void EnableBackButton() @@ -135,10 +151,15 @@ public static PageStyle GetViewStyle(Size s) } } - private string GetCountryCode() + public static string GetCountryISO() + { + var c = CultureInfo.CurrentCulture.Name; + return c.Substring(c.Length - 2); + } + + public static bool ContainsCaseInsensitive(this string str, string value) { - var c = CultureInfo.CurrentCulture.TwoLetterISOLanguageName; - return GetCountryCode(c.ToUpper()); + return CultureInfo.InvariantCulture.CompareInfo.IndexOf(str, value, CompareOptions.IgnoreCase) >= 0; } public static string GetCountryCode(string ISO3166) //https://stackoverflow.com/questions/34837436/uwp-get-country-phone-number-prefix @@ -936,4 +957,4 @@ public static class CountryArrays "ZW" }; }; -} \ No newline at end of file +} diff --git a/Signal-Windows/ViewModels/AddContactPageViewModel.cs b/Signal-Windows/ViewModels/AddContactPageViewModel.cs index 27a7c73..4e0859d 100644 --- a/Signal-Windows/ViewModels/AddContactPageViewModel.cs +++ b/Signal-Windows/ViewModels/AddContactPageViewModel.cs @@ -12,137 +12,331 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; +using System.Collections.ObjectModel; +using Windows.ApplicationModel.Core; +using libsignalservice; +using PhoneNumbers; +using System.Collections.Generic; +using System.Linq; +using Windows.UI.Xaml.Controls; +using System.Globalization; +using System.Threading; namespace Signal_Windows.ViewModels { public class AddContactPageViewModel : ViewModelBase { + public ObservableCollection Contacts; + private List signalContacts; + private PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.GetInstance(); public MainPageViewModel MainPageVM; public AddContactPage View; - private ImageSource _ContactPhoto = null; - - public ImageSource ContactPhoto - { - get { return _ContactPhoto; } - set { _ContactPhoto = value; RaisePropertyChanged(nameof(ContactPhoto)); } - } - - private string _ContactName = ""; + private string _ContactName = string.Empty; public string ContactName { get { return _ContactName; } set { _ContactName = value; RaisePropertyChanged(nameof(ContactName)); } } - private string _ContactNumber = ""; - + private string _ContactNumber = string.Empty; public string ContactNumber { get { return _ContactNumber; } set { _ContactNumber = value; RaisePropertyChanged(nameof(ContactNumber)); } } - private bool _UIEnabled = true; + private bool _ContactsVisible = true; + public bool ContactsVisible + { + get { return _ContactsVisible; } + set { _ContactsVisible = value; RaisePropertyChanged(nameof(ContactsVisible)); } + } + private bool _RefreshingContacts = false; + public bool RefreshingContacts + { + get { return _RefreshingContacts; } + set { _RefreshingContacts = value; RaisePropertyChanged(nameof(RefreshingContacts)); } + } + + public AddContactPageViewModel() + { + Contacts = new ObservableCollection(); + signalContacts = new List(); + } + + private bool _UIEnabled = true; public bool UIEnabled { get { return _UIEnabled; } set { _UIEnabled = value; RaisePropertyChanged(nameof(UIEnabled)); } } - internal void BackButton_Click(object sender, BackRequestedEventArgs e) + private bool _AddEnabled = false; + public bool AddEnabled { - if (UIEnabled) + get { return _AddEnabled; } + set { _AddEnabled = value; RaisePropertyChanged(nameof(AddEnabled)); } + } + + private bool validName = false; + private bool ValidName + { + get { return validName; } + set { - View.Frame.GoBack(); - e.Handled = true; + validName = value; + SetAddEnabled(); } } - internal async void AddButton_Click(object sender, RoutedEventArgs e) + private bool validNumber = false; + private bool ValidNumber { - if (UIEnabled) + get { return validNumber; } + set { - UIEnabled = false; - Debug.WriteLine("creating contact {0} ({1})", ContactName, ContactNumber); - SignalContact contact = new SignalContact() - { - ThreadDisplayName = ContactName, - ThreadId = ContactNumber, - CanReceive = true, - AvatarFile = null, - LastActiveTimestamp = 0, - Draft = null, - Color = "red", - UnreadCount = 0 - }; - ContactName = ""; - ContactNumber = ""; - await Task.Run(() => - { - SignalDBContext.InsertOrUpdateContactLocked(contact, MainPageVM); - }); - UIEnabled = true; + validNumber = value; + SetAddEnabled(); } } - internal async void PickButton_Click(object sender, RoutedEventArgs e) + public async Task OnNavigatedTo(CancellationToken? cancellationToken = null) { - if (UIEnabled) + ContactName = string.Empty; + ContactNumber = string.Empty; + await RefreshContacts(cancellationToken); + } + + public async Task RefreshContacts(CancellationToken? cancellationToken = null) + { + RefreshingContacts = true; + Contacts.Clear(); + signalContacts.Clear(); + SignalServiceAccountManager accountManager = new SignalServiceAccountManager(App.ServiceUrls, App.Store.Username, App.Store.Password, (int)App.Store.DeviceId, App.USER_AGENT); + ContactStore contactStore = await ContactManager.RequestStoreAsync(ContactStoreAccessType.AllContactsReadOnly); + List intermediateContacts = new List(); + if (contactStore != null) { - UIEnabled = false; - ContactPicker contactPicker = new ContactPicker(); - contactPicker.SelectionMode = ContactSelectionMode.Fields; - contactPicker.DesiredFieldsWithContactFieldType.Add(ContactFieldType.PhoneNumber); - var contact = await contactPicker.PickContactAsync(); - if (contact != null) + HashSet seenNumbers = new HashSet(); + var contacts = await contactStore.FindContactsAsync(); + ContactAnnotationStore contactAnnotationStore = await ContactManager.RequestAnnotationStoreAsync(ContactAnnotationStoreAccessType.AppAnnotationsReadWrite); + ContactAnnotationList contactAnnotationList; + var contactAnnotationLists = await contactAnnotationStore.FindAnnotationListsAsync(); + if (contactAnnotationLists.Count == 0) + { + contactAnnotationList = await contactAnnotationStore.CreateAnnotationListAsync(); + } + else + { + contactAnnotationList = contactAnnotationLists[0]; + } + + foreach (var contact in contacts) { - // The contact we just got doesn't contain the contact picture so we need to fetch it - // see https://stackoverflow.com/questions/33401625/cant-get-contact-profile-images-in-uwp - ContactStore contactStore = await ContactManager.RequestStoreAsync(ContactStoreAccessType.AllContactsReadOnly); - // If we do not have access to contacts the ContactStore will be null, we can still use the contact the user - // seleceted however - if (contactStore != null) + var phones = contact.Phones; + foreach (var phone in contact.Phones) { - Contact realContact = await contactStore.GetContactAsync(contact.Id); - if (realContact.SourceDisplayPicture != null) + if (phone.Kind == ContactPhoneKind.Mobile) { - using (var stream = await realContact.SourceDisplayPicture.OpenReadAsync()) + string formattedNumber = null; + try { - BitmapImage bitmapImage = new BitmapImage(); - await bitmapImage.SetSourceAsync(stream); - ContactPhoto = bitmapImage; + formattedNumber = ParsePhoneNumber(phone.Number); } - } - else - { - ContactPhoto = null; - } - } - ContactName = contact.Name; - if (contact.Phones.Count > 0) - { - var originalNumber = contact.Phones[0].Number; - if (originalNumber[0] != '+') - { - // need a better way of determining the "default" country code here - var formattedPhoneNumber = PhoneNumberFormatter.FormatE164("1", originalNumber); - if (string.IsNullOrEmpty(formattedPhoneNumber)) + catch (NumberParseException) { - ContactNumber = originalNumber; - MessageDialog message = new MessageDialog("Please format the number in E.164 format.", "Could not format number"); - await message.ShowAsync(); + Debug.WriteLine($"Couldn't parse {phone.Number}"); + continue; } - else + if (!seenNumbers.Contains(formattedNumber)) { - ContactNumber = formattedPhoneNumber; + seenNumbers.Add(formattedNumber); + PhoneContact phoneContact = new PhoneContact + { + Id = contact.Id, + Name = contact.FullName, + PhoneNumber = formattedNumber, + OnSignal = false + }; + if (contact.SourceDisplayPicture != null) + { + using (var stream = await contact.SourceDisplayPicture.OpenReadAsync()) + { + BitmapImage bitmapImage = new BitmapImage(); + await bitmapImage.SetSourceAsync(stream); + phoneContact.Photo = bitmapImage; + } + } + intermediateContacts.Add(phoneContact); } } } } + + // check if we've annotated a contact as a Signal contact already, if we have we don't need to ask Signal about them + for (int i = 0; i < intermediateContacts.Count; i++) + { + var annotatedContact = await contactAnnotationList.FindAnnotationsByRemoteIdAsync(intermediateContacts[i].PhoneNumber); + if (annotatedContact.Count > 0) + { + intermediateContacts[i].OnSignal = true; + signalContacts.Add(intermediateContacts[i]); + intermediateContacts.RemoveAt(i); + i--; + } + } + + var signalContactDetails = accountManager.getContacts(intermediateContacts.Select(c => c.PhoneNumber).ToList()); + foreach (var contact in intermediateContacts) + { + var foundContact = signalContactDetails.FirstOrDefault(c => c.getNumber() == contact.PhoneNumber); + if (foundContact != null) + { + contact.OnSignal = true; + ContactAnnotation contactAnnotation = new ContactAnnotation(); + contactAnnotation.ContactId = contact.Id; + contactAnnotation.RemoteId = contact.PhoneNumber; + contactAnnotation.SupportedOperations = ContactAnnotationOperations.Message | ContactAnnotationOperations.ContactProfile; + await contactAnnotationList.TrySaveAnnotationAsync(contactAnnotation); + signalContacts.Add(contact); + } + } + Contacts.AddRange(signalContacts); + } + else + { + ContactsVisible = false; + } + RefreshingContacts = false; + } + + private void SetAddEnabled() + { + AddEnabled = ValidName && ValidNumber && UIEnabled; + } + + internal void BackButton_Click(object sender, BackRequestedEventArgs e) + { + if (UIEnabled) + { + View.Frame.GoBack(); + e.Handled = true; + } + } + + internal void searchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + string text = sender.Text; + var validContacts = GetContactsMatchingText(text, signalContacts).ToList(); + Contacts.Clear(); + Contacts.AddRange(validContacts); + } + } + + private IEnumerable GetContactsMatchingText(string text, List contacts) + { + return contacts.Where( + c => c.Name.ContainsCaseInsensitive(text) || + c.PhoneNumber.ContainsCaseInsensitive(text)); + } + + internal void ContactNameTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + TextBox textBox = sender as TextBox; + string text = textBox.Text; + if (string.IsNullOrEmpty(text)) + { + ValidName = false; + } + else + { + ValidName = true; + } + } + + internal void ContactNumberTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + // TODO: See the TODO for AddButton_Click + TextBox textBox = sender as TextBox; + string text = textBox.Text; + if (string.IsNullOrEmpty(text)) + { + ValidNumber = false; + } + else + { + ValidNumber = true; + } + } + + // TODO: use the AsYouTypeFormatter when typing into the ContactNumber box so we don't have to validate here + // we need to be sure that the number here is valid + internal async Task AddButton_Click(object sender, RoutedEventArgs e) + { + if (UIEnabled) + { + UIEnabled = false; + string formattedPhoneNumber = null; + try + { + formattedPhoneNumber = ParsePhoneNumber(ContactNumber); + } + catch (NumberParseException) + { + MessageDialog message = new MessageDialog("Please format the number in E.164 format.", "Could not format number"); + await message.ShowAsync(); + return; + } + await AddContact(ContactName, formattedPhoneNumber); + UIEnabled = true; + } + } + + internal async Task ContactsList_ItemClick(object sender, ItemClickEventArgs e) + { + if (UIEnabled) + { + UIEnabled = false; + PhoneContact phoneContact = e.ClickedItem as PhoneContact; + await AddContact(phoneContact.Name, phoneContact.PhoneNumber); UIEnabled = true; } } + + private async Task AddContact(string name, string number) + { + Debug.WriteLine("creating contact {0} ({1})", name, number); + SignalContact contact = new SignalContact() + { + ThreadDisplayName = name, + ThreadId = number, + CanReceive = true, + AvatarFile = null, + LastActiveTimestamp = 0, + Draft = null, + Color = Utils.CalculateDefaultColor(name), + UnreadCount = 0 + }; + await Task.Run(() => + { + SignalDBContext.InsertOrUpdateContactLocked(contact, MainPageVM); + }); + } + + /// + /// Parses and formats a number in E164 format + /// + /// The number to parse + /// + /// A number in E164 format + private string ParsePhoneNumber(string number) + { + PhoneNumber phoneNumber = phoneNumberUtil.Parse(number, Utils.GetCountryISO()); + return phoneNumberUtil.Format(phoneNumber, PhoneNumberFormat.E164); + } } -} \ No newline at end of file +} diff --git a/Signal-Windows/Views/AddContactPage.xaml b/Signal-Windows/Views/AddContactPage.xaml index 2f7d22f..c509fce 100644 --- a/Signal-Windows/Views/AddContactPage.xaml +++ b/Signal-Windows/Views/AddContactPage.xaml @@ -5,26 +5,54 @@ xmlns:local="using:Signal_Windows.Views" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewmodels="using:Signal_Windows.ViewModels" + xmlns:controls="using:Signal_Windows.Controls" + xmlns:toolbox="using:Microsoft.Toolkit.Uwp.UI.Controls" mc:Ignorable="d" DataContext="{Binding AddContactPageInstance, Source={StaticResource Locator}}"> - - - - - - - - - - - - -