Skip to content

Commit e52aafc

Browse files
committed
use a completely virtualized stackpanel for conversations
fixes #3, closes #49, fixes #60
1 parent 9dfbce3 commit e52aafc

18 files changed

+705
-319
lines changed

Signal-Windows.sln

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,47 @@
1-
2-
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26206.0
5-
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows", "Signal-Windows\Signal-Windows.csproj", "{41736A64-5B66-44AF-879A-501192A46920}"
7-
EndProject
8-
Global
9-
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10-
Debug|ARM = Debug|ARM
11-
Debug|x64 = Debug|x64
12-
Debug|x86 = Debug|x86
13-
Release|ARM = Release|ARM
14-
Release|x64 = Release|x64
15-
Release|x86 = Release|x86
16-
EndGlobalSection
17-
GlobalSection(ProjectConfigurationPlatforms) = postSolution
18-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.ActiveCfg = Debug|ARM
19-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.Build.0 = Debug|ARM
20-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.Deploy.0 = Debug|ARM
21-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.ActiveCfg = Debug|x64
22-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.Build.0 = Debug|x64
23-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.Deploy.0 = Debug|x64
24-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.ActiveCfg = Debug|x86
25-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.Build.0 = Debug|x86
26-
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.Deploy.0 = Debug|x86
27-
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.ActiveCfg = Release|ARM
28-
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.Build.0 = Release|ARM
29-
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.Deploy.0 = Release|ARM
30-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.ActiveCfg = Release|x64
31-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.Build.0 = Release|x64
32-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.Deploy.0 = Release|x64
33-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.ActiveCfg = Release|x86
34-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Build.0 = Release|x86
35-
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Deploy.0 = Release|x86
36-
EndGlobalSection
37-
GlobalSection(SolutionProperties) = preSolution
38-
HideSolutionNode = FALSE
39-
EndGlobalSection
40-
EndGlobal
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.26730.3
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signal-Windows", "Signal-Windows\Signal-Windows.csproj", "{41736A64-5B66-44AF-879A-501192A46920}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Debug|ARM = Debug|ARM
12+
Debug|x64 = Debug|x64
13+
Debug|x86 = Debug|x86
14+
Release|Any CPU = Release|Any CPU
15+
Release|ARM = Release|ARM
16+
Release|x64 = Release|x64
17+
Release|x86 = Release|x86
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|Any CPU.ActiveCfg = Debug|x86
21+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.ActiveCfg = Debug|ARM
22+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.Build.0 = Debug|ARM
23+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|ARM.Deploy.0 = Debug|ARM
24+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.ActiveCfg = Debug|x64
25+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.Build.0 = Debug|x64
26+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x64.Deploy.0 = Debug|x64
27+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.ActiveCfg = Debug|x86
28+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.Build.0 = Debug|x86
29+
{41736A64-5B66-44AF-879A-501192A46920}.Debug|x86.Deploy.0 = Debug|x86
30+
{41736A64-5B66-44AF-879A-501192A46920}.Release|Any CPU.ActiveCfg = Release|x86
31+
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.ActiveCfg = Release|ARM
32+
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.Build.0 = Release|ARM
33+
{41736A64-5B66-44AF-879A-501192A46920}.Release|ARM.Deploy.0 = Release|ARM
34+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.ActiveCfg = Release|x64
35+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.Build.0 = Release|x64
36+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x64.Deploy.0 = Release|x64
37+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.ActiveCfg = Release|x86
38+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Build.0 = Release|x86
39+
{41736A64-5B66-44AF-879A-501192A46920}.Release|x86.Deploy.0 = Release|x86
40+
EndGlobalSection
41+
GlobalSection(SolutionProperties) = preSolution
42+
HideSolutionNode = FALSE
43+
EndGlobalSection
44+
GlobalSection(ExtensibilityGlobals) = postSolution
45+
SolutionGuid = {6C980B4F-4E4C-422E-A06F-1FB4A091E952}
46+
EndGlobalSection
47+
EndGlobal

Signal-Windows/Controls/Conversation.xaml

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,19 @@
3636
<TextBlock Name="Username" IsTextSelectionEnabled="True" HorizontalAlignment="Center" Text="{x:Bind ThreadUsername, Mode=OneWay}" Visibility="{x:Bind ThreadUsernameVisibility, Mode=OneWay}" />
3737
</StackPanel>
3838
</Border>
39-
<ScrollViewer Grid.Row="1" Name="SelectedMessagesScrollViewer" VerticalScrollBarVisibility="Visible" Padding="0 0 14 0">
40-
<ItemsControl Name="SelectedMessagesList" ItemsSource="{x:Bind Messages}" ItemTemplateSelector="{StaticResource MessageDataTemplateSelector}">
41-
<ItemsControl.ItemsPanel>
42-
<ItemsPanelTemplate>
43-
<StackPanel Orientation="Vertical" />
44-
</ItemsPanelTemplate>
45-
</ItemsControl.ItemsPanel>
46-
</ItemsControl>
47-
</ScrollViewer>
39+
<ListBox Grid.Row="1" Name="ConversationItemsControl" VirtualizingStackPanel.VirtualizationMode="Recycling" Background="White" ScrollViewer.VerticalScrollBarVisibility="Visible" Padding="0 0 15 0"> <!--ItemTemplateSelector="{StaticResource MessageDataTemplateSelector}"-->
40+
<ListBox.ItemContainerStyle>
41+
<Style TargetType="ListBoxItem">
42+
<Setter Property="Template">
43+
<Setter.Value>
44+
<ControlTemplate TargetType="ListBoxItem">
45+
<local:Message x:Name="ListBoxItemContent" />
46+
</ControlTemplate>
47+
</Setter.Value>
48+
</Setter>
49+
</Style>
50+
</ListBox.ItemContainerStyle>
51+
</ListBox>
4852
<TextBox Grid.Row="2" Name="InputTextBox" VerticalAlignment="Bottom" KeyDown="TextBox_KeyDown"></TextBox>
4953
</Grid>
5054
</UserControl>

Signal-Windows/Controls/Conversation.xaml.cs

Lines changed: 61 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ namespace Signal_Windows.Controls
2020
public sealed partial class Conversation : UserControl, INotifyPropertyChanged
2121
{
2222
public event PropertyChangedEventHandler PropertyChanged;
23-
24-
private Dictionary<long, Message> OutgoingCache = new Dictionary<long, Message>();
25-
26-
public RangeObservableCollection<object> Messages { get; set; } = new RangeObservableCollection<object>();
27-
private SignalUnreadMarker UnreadMarker = new SignalUnreadMarker();
28-
private bool UnreadMarkerAdded = false;
23+
private Dictionary<long, SignalMessageContainer> OutgoingCache = new Dictionary<long, SignalMessageContainer>();
24+
//private SignalUnreadMarker UnreadMarker = new SignalUnreadMarker();
25+
private SignalConversation SignalConversation;
26+
//private bool UnreadMarkerAdded = false;
27+
private VirtualizedCollection Collection;
2928

3029
private string _ThreadDisplayName;
3130

@@ -113,114 +112,98 @@ private void UpdateHeader(SignalConversation thread)
113112
}
114113
}
115114

116-
public void ScrollToBottom()
115+
public void Load(SignalConversation conversation)
117116
{
118-
SelectedMessagesScrollViewer.UpdateLayout();
119-
if (UnreadMarkerAdded)
120-
{
121-
var transform = UnreadMarker.View.TransformToVisual((UIElement)SelectedMessagesScrollViewer.Content);
122-
var position = transform.TransformPoint(new Point(0, 0));
123-
SelectedMessagesScrollViewer.ChangeView(null, position.Y, null, true);
124-
}
125-
else
126-
{
127-
SelectedMessagesScrollViewer.ChangeView(null, double.MaxValue, null, true);
128-
}
129-
}
130-
131-
public async Task Load(SignalConversation thread)
132-
{
133-
UnreadMarkerAdded = false;
117+
SignalConversation = conversation;
118+
//UnreadMarkerAdded = false;
134119
InputTextBox.IsEnabled = false;
135120
DisposeCurrentThread();
136-
UpdateHeader(thread);
137-
var before = Util.CurrentTimeMillis();
138-
var messages = await Task.Run(() =>
139-
{
140-
return SignalDBContext.GetMessagesLocked(thread);
141-
});
142-
var after1 = Util.CurrentTimeMillis();
143-
foreach (var message in messages)
144-
{
145-
Messages.AddSilently(message);
146-
if (thread.LastSeenMessageId == message.Id && thread.LastMessageId != message.Id)
147-
{
148-
UnreadMarkerAdded = true;
149-
Messages.AddSilently(UnreadMarker);
150-
}
151-
}
152-
Messages.ForceCollectionChanged();
121+
UpdateHeader(conversation);
122+
ConversationItemsControl.ItemsSource = new List<object>(); /* hack to avoid glitches */
153123
UpdateLayout();
154-
if (UnreadMarkerAdded)
155-
{
156-
UnreadMarker.View.SetText(thread.UnreadCount + " UNREAD MESSAGE" + (thread.UnreadCount > 1 ? "S" : ""));
157-
}
158-
foreach (var message in messages)
159-
{
160-
if (message.Direction != SignalMessageDirection.Incoming)
161-
{
162-
AddToOutgoingMessagesCache(message);
163-
}
164-
}
165-
var after2 = Util.CurrentTimeMillis();
166-
Debug.WriteLine("db query: " + (after1 - before));
167-
Debug.WriteLine("ui: " + (after2 - after1));
168-
InputTextBox.IsEnabled = thread.CanReceive;
124+
Collection = new VirtualizedCollection(conversation);
125+
ConversationItemsControl.ItemsSource = Collection;
126+
UpdateLayout();
127+
InputTextBox.IsEnabled = conversation.CanReceive;
128+
ScrollToBottom();
169129
}
170130

171131
public void DisposeCurrentThread()
172132
{
173-
Messages.Clear();
174133
OutgoingCache.Clear();
175134
}
176135

136+
public T FindElementByName<T>(FrameworkElement element, string sChildName) where T : FrameworkElement
137+
{
138+
T childElement = null;
139+
var nChildCount = VisualTreeHelper.GetChildrenCount(element);
140+
for (int i = 0; i < nChildCount; i++)
141+
{
142+
FrameworkElement child = VisualTreeHelper.GetChild(element, i) as FrameworkElement;
143+
144+
if (child == null)
145+
continue;
146+
147+
if (child is T && child.Name.Equals(sChildName))
148+
{
149+
childElement = (T)child;
150+
break;
151+
}
152+
153+
childElement = FindElementByName<T>(child, sChildName);
154+
155+
if (childElement != null)
156+
break;
157+
}
158+
return childElement;
159+
}
160+
177161
public void UpdateMessageBox(SignalMessage updatedMessage)
178162
{
179163
if (OutgoingCache.ContainsKey(updatedMessage.Id))
180164
{
181165
var m = OutgoingCache[updatedMessage.Id];
182-
m.UpdateMessageBox(updatedMessage);
166+
var item = (ListBoxItem) ConversationItemsControl.ContainerFromIndex(m.Index);
167+
var message = FindElementByName<Message>(item, "ListBoxItemContent");
168+
bool retain = message.HandleUpdate(updatedMessage);
169+
if (!retain)
170+
{
171+
OutgoingCache.Remove(m.Index);
172+
}
183173
}
184174
}
185175

186-
public void Append(SignalMessage sm)
176+
public void Append(SignalMessageContainer sm, bool forceScroll)
187177
{
188-
Messages.Add(sm);
189-
//TODO move scrolltobottom here
178+
Collection.Add(sm);
179+
if (forceScroll || true) //TODO
180+
{
181+
ScrollToBottom();
182+
}
190183
}
191184

192-
public void AddToOutgoingMessagesCache(SignalMessage sm)
185+
public void AddToOutgoingMessagesCache(SignalMessageContainer m)
193186
{
194-
if (sm.View != null)
195-
{
196-
OutgoingCache[sm.Id] = sm.View;
197-
}
198-
else
199-
{
200-
throw new Exception("Attempt to add null view to OutgoingCache");
201-
}
187+
OutgoingCache[m.Message.Id] = m;
202188
}
203189

204190
private async void TextBox_KeyDown(object sender, KeyRoutedEventArgs e)
205191
{
206192
await GetMainPageVm().TextBox_KeyDown(sender, e);
207193
}
208194

209-
public void RemoveUnreadMarker()
195+
private void ScrollToBottom()
210196
{
211-
if (UnreadMarkerAdded)
212-
{
213-
Messages.Remove(UnreadMarker);
214-
UnreadMarkerAdded = false;
215-
}
197+
var lastMsg = ConversationItemsControl.Items[ConversationItemsControl.Items.Count - 1] as SignalMessageContainer;
198+
Debug.WriteLine($"scroll to {lastMsg}");
199+
ConversationItemsControl.ScrollIntoView(lastMsg);
216200
}
217201
}
218202

219203
public class SignalUnreadMarker
220204
{
221205
public UnreadMarker View { get; set; }
222206
}
223-
224207
public class MessageTemplateSelector : DataTemplateSelector
225208
{
226209
public DataTemplate NormalMessage { get; set; }
@@ -230,19 +213,15 @@ public class MessageTemplateSelector : DataTemplateSelector
230213
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
231214
{
232215
FrameworkElement element = container as FrameworkElement;
233-
if (item is SignalMessage)
216+
if (item is SignalMessageContainer)
234217
{
235-
SignalMessage sm = (SignalMessage)item;
218+
SignalMessage sm = ((SignalMessageContainer)item).Message;
236219
if (sm.Type == SignalMessageType.IdentityKeyChange)
237220
{
238221
return IdentityKeyChangeMessage;
239222
}
240223
return NormalMessage;
241224
}
242-
if (item is SignalUnreadMarker)
243-
{
244-
return UnreadMarker;
245-
}
246225
return null;
247226
}
248227
}

Signal-Windows/Controls/IdentityKeyChangeMessage.xaml.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ public SignalMessage Model
3939

4040
private void IdentityKeyChangeMessage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
4141
{
42-
MessageTextBlock.Text = Model.Content.Content;
42+
if (Model != null)
43+
{
44+
MessageTextBlock.Text = Model.Content.Content;
45+
}
46+
else
47+
{
48+
MessageTextBlock.Text = "null";
49+
}
4350
}
4451
}
4552
}

Signal-Windows/Controls/Message.xaml

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,21 @@
1414
<BitmapImage x:Key="Check" UriSource="ms-appx:///Assets//check.png" />
1515
<BitmapImage x:Key="DoubleCheck" UriSource="ms-appx:///Assets//double-check.png" />
1616
</UserControl.Resources>
17-
<Border BorderBrush="#00000000" BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" Background="{x:Bind Background}" Padding="10 5 10 5" Margin="4">
17+
<Border Name="MessageBoxBorder" BorderBrush="#00000000" BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" Padding="10 5 10 5" Margin="4">
1818
<ItemsControl>
19-
<TextBlock Visibility="{x:Bind HeaderVisibility}" Name="MessageContentHeader" Foreground="{x:Bind ContactNameColor}" Text="{x:Bind ContactName}" FontWeight="Bold" />
20-
<TextBlock Name="MessageContentTextBlock" TextWrapping="Wrap" MaxWidth="300" IsTextSelectionEnabled="True" FontSize="14" Foreground="{x:Bind TextColor}" Text="{x:Bind Model.Content.Content}"></TextBlock>
21-
<ItemsControl Name="MessageAttachments" ItemsSource="{x:Bind Model.Attachments}">
22-
<ItemsControl.ItemTemplate>
23-
<DataTemplate x:DataType="models:SignalAttachment">
24-
<StackPanel>
25-
<TextBlock Name="AttachmentFilenameTextBlock" Text="{x:Bind SentFileName}"></TextBlock>
26-
<Image Name="AttachmentImage"></Image>
27-
<Button Name="AttachmentSaveButton" Click="AttachmentSaveButton_Click">Save</Button>
28-
</StackPanel>
29-
</DataTemplate>
30-
</ItemsControl.ItemTemplate>
31-
</ItemsControl>
19+
<TextBlock Name="MessageAuthor" FontWeight="Bold" />
20+
<TextBlock Name="MessageContentTextBlock" TextWrapping="Wrap" MaxWidth="300" IsTextSelectionEnabled="True" FontSize="14" Foreground="Black" />
3221
<Grid Name="FooterPanel">
3322
<Grid.ColumnDefinitions>
3423
<ColumnDefinition Width="Auto"/>
3524
<ColumnDefinition />
3625
<ColumnDefinition />
3726
<ColumnDefinition />
3827
</Grid.ColumnDefinitions>
39-
<TextBlock Grid.Column="0" Margin="0 0 5 0" Foreground="Red" FontWeight="SemiBold" Visibility="{x:Bind ResendVisibility, Mode=OneWay}" Tapped="ResendTextBlock_Tapped">Send again</TextBlock>
40-
<TextBlock Grid.Column="1" Foreground="{x:Bind TimestampColor}" Text="{x:Bind FancyTimestamp}" Margin="0 0 5 0" />
41-
<Image Grid.Column="2" Source="{StaticResource Check}" Visibility="{x:Bind CheckVisibility, Mode=OneWay}" Width="16" Height="16" />
42-
<Image Grid.Column="3" Source="{StaticResource DoubleCheck}" Visibility="{x:Bind DoubleCheckVisibility, Mode=OneWay}" Width="16" Height="16" />
28+
<TextBlock Grid.Column="0" Name="ResendTextBlock" Margin="0 0 5 0" Foreground="Red" FontWeight="SemiBold" Tapped="ResendTextBlock_Tapped" Visibility="Collapsed">Send again</TextBlock>
29+
<TextBlock Name="FancyTimestampBlock" Grid.Column="1" Margin="0 0 5 0" />
30+
<Image Grid.Column="2" Name="CheckImage" Source="{StaticResource Check}" Width="16" Height="16" Visibility="Collapsed"/>
31+
<Image Grid.Column="3" Name="DoubleCheckImage" Source="{StaticResource DoubleCheck}" Width="16" Height="16" Visibility="Collapsed"/>
4332
</Grid>
4433
</ItemsControl>
4534
</Border>

0 commit comments

Comments
 (0)