...
 
Commits (9)
......@@ -22,7 +22,7 @@
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.9.0-preview7" />
<PackageReference Include="Avalonia" Version="0.9.0-preview8" />
<PackageReference Include="Avalonia.Desktop" Version="0.9.0-preview7" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.0-preview7" />
<PackageReference Include="MySocialPortalLib" Version="1.0.0-alpha2" />
......
......@@ -12,10 +12,12 @@ namespace MySocialPortalDesktop.Services
public static class DefaultValueService
{
private const string DefaultProfileImageName = "default_profile_icon.png";
private static Lazy<Bitmap> DefaultProfileImageLazy = new Lazy<Bitmap>(BuildDefaultProfileImage);
public static Bitmap DefaultProfileImage = DefaultProfileImageLazy.Value;
private static readonly Lazy<Bitmap> DefaultProfileImageLazy = new Lazy<Bitmap>(BuildDefaultProfileImage);
public static Bitmap DefaultProfileImage => DefaultProfileImageLazy.Value;
public static string FavoriteListName = "Favorites";
private static Bitmap BuildDefaultProfileImage()
{
......@@ -35,6 +37,5 @@ namespace MySocialPortalDesktop.Services
var posts = RepositoryFactory.Instance.AllPostsRepository.GetPosts(maxPosts);
return new PostTimelineViewModel(posts);
}
}
}
\ No newline at end of file
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
......@@ -7,6 +8,7 @@ using JetBrains.Annotations;
using MySocialPortalDesktop.Factory;
using MySocialPortalDesktop.Services;
using MySocialPortalLib.Model;
using MySocialPortalLib.Service;
namespace MySocialPortalDesktop.Util
{
......@@ -22,8 +24,12 @@ namespace MySocialPortalDesktop.Util
}
var screenName = "";
person.SocialMediaAccounts[StandardSocialNetworkNames.Twitter]
?.AdditionalProperties.TryGetValue(screenNameKey, out screenName);
var sma = person.SocialMediaAccounts;
if (sma.ContainsKey(StandardSocialNetworkNames.Twitter))
{
person.SocialMediaAccounts[StandardSocialNetworkNames.Twitter]
?.AdditionalProperties.TryGetValue(screenNameKey, out screenName);
}
return string.IsNullOrWhiteSpace(screenName) ? unknownValue : screenName;
}
......@@ -38,7 +44,12 @@ namespace MySocialPortalDesktop.Util
try
{
var profileImageUrl = person.SocialMediaAccounts.First().Value.ProfilePhotoPath;
if (!string.IsNullOrWhiteSpace(profileImageUrl))
if (string.IsNullOrWhiteSpace(profileImageUrl))
{
return DefaultValueService.DefaultProfileImage;
}
if (Uri.IsWellFormedUriString(profileImageUrl, UriKind.Absolute))
{
var cancelToken = new CancellationTokenSource(2000);
var task = ServiceFactory.Instance.ProfileImageService
......@@ -53,6 +64,12 @@ namespace MySocialPortalDesktop.Util
}
}
}
var localImagePath = Path.Combine(DirectoryServices.Instance.ProfileImagesDirectory(), profileImageUrl);
if (File.Exists(localImagePath))
{
return new Bitmap(localImagePath);
}
}
catch (Exception e)
{
......
......@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
......@@ -63,9 +64,80 @@ namespace MySocialPortalDesktop.ViewModels
Console.WriteLine("Will import posts when get better post processor");
}
public async void ImportPeople()
{
var app = Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
var window = app.MainWindow;
var fileDialog = new OpenFileDialog
{
Directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Title = "Import People Database",
AllowMultiple = false,
Filters = new List<FileDialogFilter>
{
new FileDialogFilter
{
Name = "People JSON",
Extensions = new List<string>{"json"}
}
}
};
var result = await fileDialog.ShowAsync(window).ConfigureAwait(false);
await ImportPeopleJsonToDatabase(result?.First()).ConfigureAwait(false);
PeopleListViewModel.ShowAll(true);
Console.WriteLine("Done importing people");
}
private List<Post> ImportedPosts { get; }
private async Task<bool> ImportPeopleJsonToDatabase(string path)
{
if (path == null)
{
Console.WriteLine("Path null not doing an import");
return false;
}
if (!File.Exists(path))
{
Console.WriteLine($"Requested JSON file doesn't exist or don't have read permissions: {path}");
}
try
{
Console.WriteLine("Read in JSON");
var peopleFromJson = await Task.Run(() => System.Text.Json.JsonSerializer
.Deserialize<List<Person>>(File.ReadAllText(path)))
.ConfigureAwait(false);
Console.WriteLine("Load all people from DB");
var peopleFromDb = await Task.Run(() => RepositoryFactory.Instance.MainPeopleRepository
.ListAll())
.ConfigureAwait(false);
Console.WriteLine("Merge two sets of people");
var (newPersons, mergedPersons, _) = PersonDatabaseMergingUtils.Merge(peopleFromDb, peopleFromJson);
Console.WriteLine($"New People: {newPersons?.Count}");
Console.WriteLine($"Merged People: {mergedPersons?.Count}");
Console.WriteLine("Write new people to the database");
await Task.Run(() => newPersons.ToList()
.ForEach(p => RepositoryFactory.Instance.MainPeopleRepository.AddPerson(p)))
.ConfigureAwait(false);
Console.WriteLine("Update existing people in database");
await Task.Run(() => mergedPersons.ToList()
.ForEach(p => RepositoryFactory.Instance.MainPeopleRepository.UpdatePerson(p)))
.ConfigureAwait(false);
Console.WriteLine("Update People list view");
}
catch (Exception e)
{
Console.WriteLine(e);
return false;
}
return true;
}
}
}
......@@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using MySocialPortalDesktop.Comparer;
using MySocialPortalDesktop.Factory;
using MySocialPortalLib.Model;
using ReactiveUI;
namespace MySocialPortalDesktop.ViewModels
......@@ -26,15 +27,11 @@ namespace MySocialPortalDesktop.ViewModels
public PeopleListViewModel(PostTimelineViewModel relatedTimelineViewModel)
{
var allPeople = RepositoryFactory.Instance.MainPeopleRepository.ListAll();
var peopleVms = allPeople?.Select(p => new PersonViewModel(p)).ToList() ?? new List<PersonViewModel>();
if (relatedTimelineViewModel != null)
{
peopleVms.ForEach(p => p.TimelineToDrive = relatedTimelineViewModel);
}
RelatedTimelineViewModel = relatedTimelineViewModel;
var peopleVms = GetAllPersonsFromDatabase();
People = new ObservableCollection<PersonViewModel>(peopleVms);
FullPeopleList = new List<PersonViewModel>(peopleVms);
ListFilteredPeople = new List<PersonViewModel>(peopleVms);
CurrentViewFilter = FilterText = "";
}
......@@ -61,13 +58,38 @@ namespace MySocialPortalDesktop.ViewModels
UpdateSortedView();
}
public void ShowAll(bool resetFromDb = false)
{
if (resetFromDb)
{
FullPeopleList.Clear();
FullPeopleList.AddRange(GetAllPersonsFromDatabase());
}
ListFilteredPeople.Clear();
ListFilteredPeople.AddRange(FullPeopleList);
UpdateViewFilter(true);
}
public void ToggleVisibleList(string list)
{
var idsInList = RepositoryFactory.Instance.ListsRepository.GetAllIdsForList(list).ToHashSet();
var peopleVmsInList = FullPeopleList.FindAll(p => idsInList.Contains(p.Person.Id));
ListFilteredPeople.Clear();
ListFilteredPeople.AddRange(peopleVmsInList);
UpdateViewFilter(true);
}
private IComparer<PersonViewModel> CurrentComparer { get; set; }
private string CurrentViewFilter { get; set; }
private List<PersonViewModel> ListFilteredPeople { get; }
private List<PersonViewModel> FullPeopleList { get; }
private PostTimelineViewModel RelatedTimelineViewModel { get; }
private void UpdateSortedView()
{
if(CurrentComparer == null)
......@@ -90,7 +112,7 @@ namespace MySocialPortalDesktop.ViewModels
CurrentViewFilter = FilterText;
var filter = FilterText.ToUpperInvariant();
var filteredPeople = FilterText.Length == 0 ? new List<PersonViewModel>(FullPeopleList)
var filteredPeople = FilterText.Length == 0 ? new List<PersonViewModel>(ListFilteredPeople)
: FullPeopleList.FindAll(
f => f.DisplayName.ToUpperInvariant().Contains(filter, StringComparison.InvariantCulture)
|| f.DisplayName.Contains(filter, StringComparison.InvariantCulture));
......@@ -103,5 +125,16 @@ namespace MySocialPortalDesktop.ViewModels
filteredPeople.ForEach(f => People.Add(f));
}
private List<PersonViewModel> GetAllPersonsFromDatabase()
{
var allPeople = RepositoryFactory.Instance.MainPeopleRepository.ListAll();
var peopleVms = allPeople?.Select(p => new PersonViewModel(p)).ToList() ?? new List<PersonViewModel>();
if (RelatedTimelineViewModel != null)
{
peopleVms.ForEach(p => p.TimelineToDrive = RelatedTimelineViewModel);
}
return peopleVms;
}
}
}
\ No newline at end of file
using System;
using System.Linq;
using Avalonia.Media.Imaging;
using MySocialPortalDesktop.Factory;
using MySocialPortalDesktop.Services;
using MySocialPortalDesktop.Util;
using MySocialPortalLib.Model;
......@@ -15,6 +16,7 @@ namespace MySocialPortalDesktop.ViewModels
private Bitmap _displayIcon;
private Person _person;
private PostTimelineViewModel _timelineToDrive;
private bool _isFavorite;
public PostTimelineViewModel TimelineToDrive
{
......@@ -40,6 +42,12 @@ namespace MySocialPortalDesktop.ViewModels
set => this.RaiseAndSetIfChanged(ref _displayIcon, value);
}
public bool IsFavorite
{
get => _isFavorite;
set => this.RaiseAndSetIfChanged(ref _isFavorite, value);
}
public Person Person
{
get => _person;
......@@ -60,12 +68,27 @@ namespace MySocialPortalDesktop.ViewModels
Console.WriteLine($"Change timeline to be for user {Person.Name}");
TimelineToDrive?.ConfigureSources(Person);
}
public void AddToFavorites()
{
RepositoryFactory.Instance.ListsRepository.Add(Person.Id, DefaultValueService.FavoriteListName);
IsFavorite = true;
}
public void RemoveFromFavorites()
{
RepositoryFactory.Instance.ListsRepository.RemoveIdFromList(Person.Id, DefaultValueService.FavoriteListName);
IsFavorite = false;
}
private void UpdatePersonData()
{
DisplayName = string.IsNullOrWhiteSpace(Person.Name) ? "" : Person.Name;
DisplayUsername = PersonDataGenerator.GetDefaultUserName(Person).Result;
DisplayIcon = PersonDataGenerator.LoadProfileImage(Person).Result;
IsFavorite = RepositoryFactory.Instance.ListsRepository
.GetAllListsForId(Person.Id)
.Contains(DefaultValueService.FavoriteListName);
}
}
......
......@@ -12,7 +12,7 @@ namespace MySocialPortalDesktop.ViewModels
{
public class PostTimelineViewModel : ViewModelBase
{
private const int MaxPostsQuery = 5;
private const int MaxPostsQuery = 20;
private const int MaxPostHistory = 50;
public ObservableCollection<PostViewModel> PostViewModels { get; }
......@@ -57,7 +57,7 @@ namespace MySocialPortalDesktop.ViewModels
try
{
var posts = CurrentPerson == null ?
TwitterConnector.GetNewerHomeTimeline(MaxPostsQuery).ToList() :
TwitterConnector.GetNewerHomeTimeline(10).ToList() :
TwitterConnector.GetNewerUserTimeline(CurrentPerson, MaxPostsQuery).ToList();
ProcessNewPosts(posts, true);
......@@ -73,7 +73,7 @@ namespace MySocialPortalDesktop.ViewModels
try
{
var posts = CurrentPerson == null ?
TwitterConnector.GetOlderHomeTimeline(MaxPostsQuery).ToList() :
TwitterConnector.GetOlderHomeTimeline(10).ToList() :
TwitterConnector.GetOlderUserTimeline(CurrentPerson, MaxPostsQuery).ToList();
ProcessNewPosts(posts, false);
}
......@@ -110,33 +110,48 @@ namespace MySocialPortalDesktop.ViewModels
RepositoryFactory.Instance.AllPostsRepository.AddPosts(filteredPosts);
if (insertTop)
{
posts.Sort(new PostComparisonAscending());
posts.Sort(new PostComparison(true));
posts.ForEach(p => PostViewModels.Insert(0, new PostViewModel(p)));
}
else
{
posts.Sort(new PostComparisonDescending());
var currentGuessIndex = 0;
posts.Sort(new PostComparison(false));
foreach (var post in posts)
{
var postDate = post.PostDateTime.LocalDateTime;
var guessDate = PostViewModels[currentGuessIndex].Date;
if (postDate >= guessDate)
var found = false;
var currentGuessIndex = 0;
while (!found)
{
PostViewModels.Insert(currentGuessIndex, new PostViewModel(post));
continue;
}
if (PostViewModels.Count == 0)
{
PostViewModels.Add(new PostViewModel(post));
found = true;
continue;
}
var postDate = post.PostDateTime.LocalDateTime;
var guessDate = PostViewModels[currentGuessIndex].Date;
if (currentGuessIndex == PostViewModels.Count - 1)
{
PostViewModels.Insert(currentGuessIndex, new PostViewModel(post));
continue;
}
if (postDate >= guessDate)
{
PostViewModels.Insert(currentGuessIndex, new PostViewModel(post));
found = true;
continue;
}
if (currentGuessIndex == PostViewModels.Count - 1)
{
PostViewModels.Insert(currentGuessIndex, new PostViewModel(post));
found = true;
continue;
}
currentGuessIndex++;
currentGuessIndex++;
}
}
PostViewModels.AddRange(posts.Select(p => new PostViewModel(p)));
//PostViewModels.AddRange(posts.Select(p => new PostViewModel(p)));
}
}
}
......
......@@ -17,7 +17,8 @@
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Posts Import" Command="{Binding ImportPosts}" HotKey="CTRL+H"/>
<MenuItem Header="_Posts Import" Command="{Binding ImportPosts}" HotKey="CTRL+P"/>
<MenuItem Header="P_eople Import" Command="{Binding ImportPeople}" HotKey="CTRL+E"/>
<Separator/>
<MenuItem Header="Exit" Command="{Binding DoExit}"/>
</MenuItem>
......
......@@ -16,8 +16,17 @@
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*">
<Grid Grid.Row="0" Grid.Column="0" RowDefinitions="Auto" ColumnDefinitions="Auto,*" Margin="2">
<Grid RowDefinitions="Auto, Auto,*" ColumnDefinitions="*">
<Grid Grid.Row="0" Grid.Column="0">
<Grid RowDefinitions="*" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Visible Users: "/>
<ComboBox Name="VisibleUsersSelection" Grid.Row="0" Grid.Column="1" SelectedIndex="0">
<ComboBoxItem Content="All"/>
<ComboBoxItem Content="Favorites"/>
</ComboBox>
</Grid>
</Grid>
<Grid Grid.Row="1" Grid.Column="0" RowDefinitions="Auto" ColumnDefinitions="Auto,*" Margin="2">
<ComboBox Name="SortOrderComboBox" Grid.Row="0" Grid.Column="0" Width="100" SelectedIndex="0" >
<ComboBoxItem Content="None" />
<ComboBoxItem Content="Name (A-Z)" />
......@@ -27,13 +36,16 @@
</ComboBox>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FilterText}" />
</Grid>
<ListBox Grid.Row="1" Grid.Column="0" Name="FriendsList" Items="{Binding People}">
<ListBox Grid.Row="2" Grid.Column="0" Name="FriendsList" Items="{Binding People}">
<ListBox.DataTemplates>
<DataTemplate DataType="vm:PersonViewModel">
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, Auto">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="View Timeline" Command="{Binding ViewTimeline}"/>
<MenuItem Header="-"></MenuItem>
<MenuItem Header="Add To Favorites" Command="{Binding AddToFavorites}" IsEnabled="{Binding !IsFavorite}"/>
<MenuItem Header="Remove From Favorites" Command="{Binding RemoveFromFavorites}" IsEnabled="{Binding IsFavorite}"/>
</ContextMenu>
</Grid.ContextMenu>
<Image Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Margin="5" Source="{Binding DisplayIcon}" Width="50" Height="50"/>
......
......@@ -32,6 +32,28 @@ namespace MySocialPortalDesktop.Views
var selection = (e?.AddedItems[0] as ComboBoxItem)?.Content?.ToString();
vm.ChangeSortOrderCommand(selection);
};
var visibleUsers = this.Find<ComboBox>("VisibleUsersSelection");
visibleUsers.SelectionChanged += (sender, e) =>
{
var vm = this.DataContext as PeopleListViewModel;
if (vm == null)
{
return;
}
var selection = (e?.AddedItems[0] as ComboBoxItem)?.Content?.ToString();
if (selection == "All")
{
vm.ShowAll();
}
else
{
vm.ToggleVisibleList(selection);
}
};
}
}
}
\ No newline at end of file
using System;
using System.IO;
using LiteDB;
using MySocialPortalLib.Repository;
using MySocialPortalLib.Service;
......@@ -10,6 +11,7 @@ namespace MySocialPortalDesktop.Factory
private const string AllPostsRepositoryDbName = "all_posts.db";
private const string LinkPreviewRepositoryDbName = "link_preview.db";
private const string LinkPreviewCacheRepositoryDbName = "link_preview_cache.db";
private const string ListsRepositoryDbName = "lists.db";
private const string MainPeopleRepositoryDbName = "all_people.db";
private const string ProfileImageRepositoryDbName = "profile_images.db";
private const string TimelineRepositoryDbName = "timeline_cache.db";
......@@ -27,6 +29,8 @@ namespace MySocialPortalDesktop.Factory
public IFileCacheRepository LinkPreviewImageCacheRepository => LinkPreviewImageCacheRepositoryLazy.Value;
public INamedListRepository ListsRepository => ListsRepositoryLazy.Value;
public IFileCacheRepository ProfileImageCacheRepository => ProfileImageCacheRepositoryLazy.Value;
public IPersonsRepository MainPeopleRepository => MainPeopleRepositoryLazy.Value;
......@@ -38,6 +42,8 @@ namespace MySocialPortalDesktop.Factory
AllPostsRepositoryLazy = new Lazy<IPostsRepository>(BuildAllPostsRepository);
LinkPreviewRepositoryLazy = new Lazy<ILinkPreviewRepository>(BuildLinkPreviewRepository);
LinkPreviewImageCacheRepositoryLazy = new Lazy<IFileCacheRepository>(BuildLinkPreviewImageCacheRepository);
ListsRepositoryLazy = new Lazy<INamedListRepository>(BuildListRepository);
ListsRepositoryBackingLazy = new Lazy<LiteRepository>(BuildListRepositoryBacking);
MainPeopleRepositoryLazy = new Lazy<IPersonsRepository>(BuildPeopleRepository);
ProfileImageCacheRepositoryLazy = new Lazy<IFileCacheRepository>(BuildProfileImageCacheRepository);
TimelineRepositoryLazy = new Lazy<ITimelineRepository>(BuildTimelineRepository);
......@@ -49,6 +55,10 @@ namespace MySocialPortalDesktop.Factory
private Lazy<IFileCacheRepository> LinkPreviewImageCacheRepositoryLazy { get; }
private Lazy<INamedListRepository> ListsRepositoryLazy { get; }
private Lazy<LiteRepository> ListsRepositoryBackingLazy { get; }
private Lazy<IPersonsRepository> MainPeopleRepositoryLazy { get; }
private Lazy<IFileCacheRepository> ProfileImageCacheRepositoryLazy { get; }
......@@ -69,6 +79,16 @@ namespace MySocialPortalDesktop.Factory
{
return new FileCacheLiteDbRepository(BuildDbPath(LinkPreviewCacheRepositoryDbName));
}
private INamedListRepository BuildListRepository()
{
return new NamedListLiteDbRepository(ListsRepositoryBackingLazy.Value);
}
private LiteRepository BuildListRepositoryBacking()
{
return new LiteRepository(BuildDbPath(ListsRepositoryDbName));
}
private IFileCacheRepository BuildProfileImageCacheRepository()
{
......@@ -106,6 +126,7 @@ namespace MySocialPortalDesktop.Factory
(ProfileImageCacheRepository as IDisposable)?.Dispose();
(TimelineRepository as IDisposable)?.Dispose();
(LinkPreviewImageCacheRepository as IDisposable)?.Dispose();
(ListsRepositoryBackingLazy.Value as IDisposable)?.Dispose();
}
_disposed = true;
......
......@@ -54,6 +54,26 @@ namespace MySocialPortalLib.Model
&& SocialMediaSystemName == other.SocialMediaSystemName;
}
public SocialMediaAccountData Copy()
{
var newValue = new SocialMediaAccountData
{
Id = this.Id,
Active = this.Active,
ProfileId = this.ProfileId,
ProfilePhotoPath = this.ProfilePhotoPath,
ProfileUrl = this.ProfileUrl,
RealName = this.RealName,
SocialMediaSystemName = this.SocialMediaSystemName
};
foreach (var (k,v) in AdditionalProperties)
{
newValue.AdditionalProperties[k] = v;
}
return newValue;
}
protected bool Equals(SocialMediaAccountData other)
{
if (other == null)
......
using System.Collections.Generic;
namespace MySocialPortalLib.Repository
{
public interface INamedListRepository
{
void Add(string id, string listName);
void RemoveIdFromList(string id, string listName);
void RemoveList(string listName);
void RemoveId(string id);
IList<string> GetAllLists();
IList<string> GetAllListsForId(string id);
IList<string> GetAllIdsForList(string list);
}
}
\ No newline at end of file
using System;
using System.Collections.Generic;
using System.Linq;
using LiteDB;
namespace MySocialPortalLib.Repository
{
public class NamedListLiteDbRepository : INamedListRepository
{
private const string DefaultCollectionName = "NamedLists";
private NamedListLiteDbRepository()
{
throw new MethodAccessException();
}
public NamedListLiteDbRepository(LiteRepository repository, string collectionName = DefaultCollectionName)
{
Repository = repository;
CollectionName = collectionName;
}
public void Add(string id, string listName)
{
var entry = new Entry
{
ListName = listName,
MemberId = id
};
if (Repository.Query<Entry>(CollectionName)
.Where(e => e.ListName == listName && e.MemberId == id)
.Count() == 0)
{
Repository.Insert(entry, CollectionName);
}
}
public IList<string> GetAllLists()
{
return Repository.Database.Execute($"SELECT DISTINCT(*.ListName) From {CollectionName}")
.Current["expr"]
.AsArray
.Select(e => e.AsString)
.ToList();
}
public IList<string> GetAllListsForId(string id)
{
return Repository.Query<Entry>(CollectionName)
.Where(e => e.MemberId == id)
.ToList()
.Select(e => e.ListName)
.ToList();
}
public IList<string> GetAllIdsForList(string list)
{
return Repository.Query<Entry>(CollectionName)
.Where(e => e.ListName == list)
.ToList()
.Select(e => e.MemberId)
.ToList();
}
public void RemoveIdFromList(string id, string listName)
{
Repository.DeleteMany<Entry>(e => e.MemberId == id && e.ListName == listName, CollectionName);
}
public void RemoveList(string listName)
{
Repository.DeleteMany<Entry>(e => e.ListName == listName, CollectionName);
}
public void RemoveId(string id)
{
Repository.DeleteMany<Entry>(e => e.MemberId == id, CollectionName);
}
private LiteRepository Repository { get; }
private string CollectionName { get; }
private class Entry
{
public string Id { get; set; }
public string ListName { get; set; }
public string MemberId { get; set; }
public Entry()
{
Id = Guid.NewGuid().ToString();
}
}
}
}
\ No newline at end of file
......@@ -46,7 +46,8 @@ namespace MySocialPortalLib.Repository
ulong stopValue,
string serviceName = DefaultServiceName)
{
return TimelineRepository.Query<TimelineInterval>(serviceName)
//BSON doesn't have ulong type so need to bring the whole collection back and do the LINQ query at client
return TimelineRepository.Query<TimelineInterval>(serviceName).ToList()
.Where(i => (i.IntervalStart >= startValue && i.IntervalStop <= stopValue)
|| (i.IntervalStart < startValue && i.IntervalStop >= startValue && i.IntervalStop <= stopValue)
|| (i.IntervalStop > stopValue && i.IntervalStart >= startValue && i.IntervalStart <= stopValue))
......@@ -58,8 +59,9 @@ namespace MySocialPortalLib.Repository
ulong stopValue,
string serviceName = DefaultServiceName)
{
//BSON doesn't have ulong type so need to bring the whole collection back and do the LINQ query at client
return TimelineRepository.Query<TimelineInterval>(serviceName)
.Where(i => i.TimelineName == timelineName)
.Where(i => i.TimelineName == timelineName).ToList()
.Where(i => (i.IntervalStart >= startValue && i.IntervalStop <= stopValue)
|| (i.IntervalStart < startValue && i.IntervalStop >= startValue && i.IntervalStop <= stopValue)
|| (i.IntervalStop > stopValue && i.IntervalStart >= startValue && i.IntervalStart <= stopValue))
......
......@@ -172,39 +172,48 @@ namespace MySocialPortalLib.Service.SocialMediaConnectors
private IList<Post> PullHomeTweets(TimelineInterval interval, int maxPosts)
{
IQueryable<Status> query;
var updateUpper = true;
var removeIntervalIfEmpty = false;
Console.WriteLine($"PullHomeTweets {maxPosts} #posts for {interval}");
if (interval.IntervalStop == ulong.MaxValue)
{
Console.WriteLine("Pulling home tweets with no upper bound");
query = GetNoUpperHomeStatusQuery(interval, maxPosts);
removeIntervalIfEmpty = true;
}
else
{
Console.WriteLine("Pulling upper and lower bound tweets");
query = GetUpperLowerHomeStatusQuery(interval, maxPosts);
updateUpper = false;
}
return PullTweets(interval, maxPosts, query);
return PullTweets(interval, maxPosts, query, updateUpper, removeIntervalIfEmpty);
}
private IList<Post> PullUserTweets(string username, TimelineInterval interval, int maxPosts)
{
IQueryable<Status> query;
bool updateUpper = true;
bool removeIntervalIfEmpty = false;
if (interval.IntervalStop == ulong.MaxValue)
{
Console.WriteLine($"Pulling tweets for Twitter user {username} with no upper bounds: {interval}");
query = GetNoUpperUserStatusQuery(username, interval, maxPosts);
removeIntervalIfEmpty = true;
}
else
{
Console.WriteLine($"Pulling tweets for Twitter user {username} with no upper and lower bounds: {interval}");
Console.WriteLine($"Pulling tweets for Twitter user {username} with upper and lower bounds: {interval}");
query = GetUpperLowerUserStatusQuery(username, interval, maxPosts);
updateUpper = false;
}
return PullTweets(interval, maxPosts, query);
return PullTweets(interval, maxPosts, query, updateUpper, removeIntervalIfEmpty);
}
private IList<Post> PullTweets(TimelineInterval newInterval, int maxPosts, IQueryable<Status> query)
private IList<Post> PullTweets(TimelineInterval newInterval, int maxPosts, IQueryable<Status> query, bool updateUpper, bool removeRequestIfEmpty)
{
var posts = new List<Post>();
if (newInterval == null)
......@@ -217,21 +226,30 @@ namespace MySocialPortalLib.Service.SocialMediaConnectors
Console.WriteLine($"{tweets.Count} tweets returned");
if (tweets.Count == 0)
{
if (removeRequestIfEmpty)
{
TimelineManager.RemoveRequestInterval(newInterval);
}
return posts;
}
UpdateSampleInterval(tweets, newInterval, maxPosts);
UpdateSampleInterval(tweets, newInterval, maxPosts, updateUpper);
var converter = new TwitterConverter(PersonsRepository);
posts.AddRange(tweets.Select(t => converter.TweetToPost(t)));
return posts;
}
private void UpdateSampleInterval(IList<Status> tweets, TimelineInterval newInterval, int maxPosts)
private void UpdateSampleInterval(IList<Status> tweets, TimelineInterval newInterval, int maxPosts, bool updateUpper)
{
var earliestTweet = tweets.Min(t => t.StatusID);
var latestTweet = tweets.Max(t => t.StatusID);
Console.WriteLine($"TweetIDs range: {earliestTweet} to {latestTweet}");
newInterval.IntervalStop = latestTweet;
if (updateUpper)
{
newInterval.IntervalStop = latestTweet;
}
if (tweets.Count == maxPosts)
{
Console.WriteLine("Max posts returned therefore setting interval start as well");
......
......@@ -136,12 +136,18 @@ namespace MySocialPortalLib.Service
return success;
}
public bool RemoveRequestInterval(TimelineInterval interval)
{
return TimelineRepository.Remove(interval, ServiceName);
}
private ITimelineRepository TimelineRepository { get; }
private void CleanupIntervals(string timelineName)
{
var gapTolerance = 2UL;
var allIntervals = TimelineRepository.FindByTimelineName(timelineName, ServiceName);
var mergedIntervals = TimelineIntervalUtilities.CleanupList(allIntervals);
var mergedIntervals = TimelineIntervalUtilities.CleanupList(allIntervals, gapTolerance);
TimelineRepository.RemoveByTimelineName(timelineName, ServiceName);
TimelineRepository.AddOrUpdate(mergedIntervals, ServiceName);
}
......
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using MySocialPortalLib.Model;
namespace MySocialPortalLib.Util
{
public static class PersonDatabaseMergingUtils
{
public static (IList<Person> newPersons, IList<Person> mergedPersons, IList<Person> unchangedPersons) Merge(
IList<Person> list1,
IList<Person> list2)
{
var originalList1 = new List<Person>(list1);
var originalList2 = new List<Person>(list2);
var mergedPersons = new List<Person>();
var list1Dictionary = originalList1.ToImmutableDictionary(p => p.Id, p => p, StringComparer.InvariantCulture);
var list2Dictionary = originalList2.ToImmutableDictionary(p => p.Id, p => p, StringComparer.InvariantCulture);
var commonIds = list1Dictionary.Keys.Intersect(list2Dictionary.Keys);
foreach (var id in commonIds)
{
var person1 = list1Dictionary[id];
var person2 = list2Dictionary[id];
originalList1.Remove(person1);
originalList2.Remove(person2);
person1.Merge(person2);
mergedPersons.Add(person1);
}
var socialNetworkNameDict1 = originalList1.ToSocialNetworkUsernameKeyedDictionary();
var socialNetworkNameDict2 = originalList2.ToSocialNetworkUsernameKeyedDictionary();
var commonNetworkIds = socialNetworkNameDict1.Keys.Intersect(socialNetworkNameDict2.Keys);
foreach (var networkId in commonNetworkIds)
{
var person1 = socialNetworkNameDict1[networkId];
var person2 = socialNetworkNameDict2[networkId];
originalList1.Remove(person1);
originalList2.Remove(person2);
person1.Merge(person2);
if (!mergedPersons.Contains(person1))
{
mergedPersons.Add(person1);
}
}
return (originalList2, mergedPersons, originalList1);
}
public static void Merge(this Person person1, Person person2)
{
person1.Emails.AddIfNew(person2.Emails);
person1.Addresses.AddIfNew(person2.Addresses);
person1.Websites.AddIfNew(person2.Websites);
person1.PhoneNumbers.AddIfNew(person2.PhoneNumbers);
person1.SocialMediaAccounts.AddIfNew(person2.SocialMediaAccounts);
foreach (var k in person2.AdditionalProperties.Keys)
{
person1.AdditionalProperties[k] = person2.AdditionalProperties[k];
}
}
public static void AddIfNew(this List<LabeledValue<string>> currentSet, LabeledValue<string> newValue)
{
var sameValue = currentSet.Find(v => v.Label == newValue.Label);
if (sameValue == null)
{
currentSet.Add(newValue);
return;
}
sameValue.Value = string.IsNullOrWhiteSpace(sameValue.Value) ? newValue.Value : sameValue.Value;
}
public static void AddIfNew(this List<LabeledValue<string>> currentSet, List<LabeledValue<string>> newValues)
{
newValues.ForEach(v => currentSet.AddIfNew(v));
}
public static void AddIfNew(this Dictionary<string, SocialMediaAccountData> currentSet,
Dictionary<string, SocialMediaAccountData> newValues)
{
foreach (var (key, value) in newValues)
{
if (currentSet.TryGetValue(key, out var smd))
{
smd.Merge(value);
continue;
}
currentSet.Add(key, value);
}
}
public static void Merge(this SocialMediaAccountData cv, SocialMediaAccountData nv,
bool flushAdditionalProps = false)
{
cv.Active = nv.Active;
if (!string.IsNullOrEmpty(nv.ProfileId))
{
cv.ProfileId = nv.ProfileId;
}
if (!string.IsNullOrEmpty(nv.ProfileUrl))
{
cv.ProfileUrl = nv.ProfileUrl;
}
if (!string.IsNullOrEmpty(nv.RealName))
{
cv.RealName = nv.RealName;
}
if (!string.IsNullOrEmpty(nv.ProfilePhotoPath))
{
cv.ProfilePhotoPath = nv.ProfilePhotoPath;
}
if (!string.IsNullOrEmpty(nv.SocialMediaSystemName))
{
cv.SocialMediaSystemName = nv.SocialMediaSystemName;
}
if (flushAdditionalProps)
{
cv.AdditionalProperties.Clear();
}
foreach (var (k,v) in nv.AdditionalProperties)
{
cv.AdditionalProperties[k] = v;
}
}
private static IDictionary<string, Person> ToSocialNetworkUsernameKeyedDictionary(this IEnumerable<Person> persons)
{
var results = new Dictionary<string, Person>();
foreach (var p in persons)
{
foreach (var (network, smd) in p.SocialMediaAccounts)
{
if (string.IsNullOrWhiteSpace(network) || string.IsNullOrWhiteSpace(smd.ProfileId))
{
continue;
}
var key = $"{network}{smd.ProfileId}";
results[key] = p;
}
}
return results;
}
}
}
\ No newline at end of file
......@@ -3,16 +3,22 @@ using MySocialPortalLib.Model;
namespace MySocialPortalLib.Util
{
public class PostComparisonAscending: IComparer<Post>
public class PostComparison: IComparer<Post>
{
private readonly int _multiplier;
public PostComparison(bool ascending)
{
_multiplier = ascending ? 1 : -1;
}
public int Compare(Post x, Post y)
{
if (x == null || y == null)
{
return 0;
}
return x.PostDateTime.CompareTo(y.PostDateTime);
return _multiplier * x.PostDateTime.CompareTo(y.PostDateTime);
}
}
}
\ No newline at end of file
......@@ -32,6 +32,12 @@ namespace MySocialPortalLibTest.Factory
Assert.NotNull(RepositoryFactory.Instance.LinkPreviewImageCacheRepository);
}
[Fact]
public void TestListsRepository()
{
Assert.NotNull(RepositoryFactory.Instance.ListsRepository);
}
[Fact]
public void TestMainPeopleRepository()
{
......
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json;
......@@ -18,6 +19,29 @@ namespace MySocialPortalLibTest.Model
Assert.True(d.AllEquals(fromJson));
}
[Fact]
public void TestCopy()
{
var emptySma = new SocialMediaAccountData();
Assert.True(emptySma.AllEquals(emptySma.Copy()));
var sma = new SocialMediaAccountData
{
Active = false,
ProfileId = Guid.NewGuid().ToString(),
ProfileUrl = Guid.NewGuid().ToString(),
RealName = Guid.NewGuid().ToString(),
ProfilePhotoPath = Guid.NewGuid().ToString(),
SocialMediaSystemName = Guid.NewGuid().ToString()
};
sma.AdditionalProperties[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
sma.AdditionalProperties[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
var smaCopy = sma.Copy();
Assert.True(sma.AllEquals(smaCopy));
sma.AdditionalProperties.Clear();
Assert.False(sma.AllEquals(smaCopy));
}
}
public class SocialMediaAccountDataDataGenerator : IEnumerable<object[]>
......
using System.IO;
using LiteDB;
using MySocialPortalLib.Repository;
using Xunit;
namespace MySocialPortalLibTest.Repository
{
public class NamedListLiteDbRepositoryTest
{
[Fact]
public void TestInsertionAndQuery()
{
var repo = GetRepo();
repo.Add("id2", "list1");
repo.Add("id1", "list1");
var ids = repo.GetAllIdsForList("list1");
Assert.Equal(3, ids.Count);
Assert.Contains("id1", ids);
Assert.Contains("id2", ids);
Assert.Contains("id3", ids);
Assert.Empty(repo.GetAllIdsForList("list5"));
Assert.Empty(repo.GetAllIdsForList(""));
Assert.Empty(repo.GetAllIdsForList(null));
}
[Fact]
public void TestGetListNames()
{
var repo = GetRepo();
var lists = repo.GetAllLists();
Assert.Equal(2, lists.Count);
Assert.Contains("list1", lists);
Assert.Contains("list2", lists);
}
[Fact]
public void TestGetListForId()
{
var repo = GetRepo();
var lists = repo.GetAllListsForId("id1");
Assert.Equal(2, lists.Count);
Assert.Contains("list1", lists);
Assert.Contains("list2", lists);
Assert.Empty(repo.GetAllListsForId("id20"));
Assert.Empty(repo.GetAllListsForId(""));
Assert.Empty(repo.GetAllListsForId(null));
}
[Fact]
public void TestRemoveIdFromList()
{
var repo = GetRepo();
repo.RemoveIdFromList("id1", "list1");
Assert.DoesNotContain("id1", repo.GetAllIdsForList("list1"));
Assert.DoesNotContain("list1", repo.GetAllListsForId("id1"));
repo.RemoveIdFromList("","");
repo.RemoveIdFromList(null,null);
}
[Fact]
public void TestRemoveList()
{
var repo = GetRepo();
repo.RemoveList("list1");
Assert.DoesNotContain("list1", repo.GetAllLists());
Assert.DoesNotContain("list1", repo.GetAllListsForId("id1"));
repo.RemoveList("Someotherlist");
}
[Fact]
public void TestRemoveId()
{
var repo = GetRepo();
repo.RemoveId("id1");
Assert.DoesNotContain("id1", repo.GetAllIdsForList("list1"));
repo.RemoveId("aoeuaeuaoe");
repo.RemoveId(null);
repo.RemoveId("");
}
private NamedListLiteDbRepository GetRepo()
{
var fileStream = new FileStream(Path.GetTempFileName(), FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.None, 4096,
FileOptions.RandomAccess | FileOptions.DeleteOnClose);
var repo = new LiteRepository(fileStream);
var listRepo = new NamedListLiteDbRepository(repo);
listRepo.Add("id1", "list1");
listRepo.Add("id1", "list2");
listRepo.Add("id3", "list1");
return listRepo;
}
}
}
\ No newline at end of file
......@@ -107,47 +107,48 @@ namespace MySocialPortalLibTest.Repository
public void TestFindByInterval()
{
var db = GetTempDb();
var baseIntervalValue = ulong.MaxValue - 10000;
var interval1 = new TimelineInterval
{
IntervalStart = 4,
IntervalStop = 6,
IntervalStart = baseIntervalValue + 4,
IntervalStop = baseIntervalValue + 6,
TimelineName = "Name1"
};
db.AddOrUpdate(interval1);
var interval2 = new TimelineInterval
{
IntervalStart = 8,
IntervalStop = 9,
IntervalStart = baseIntervalValue + 8,
IntervalStop = baseIntervalValue + 9,
TimelineName = "Name1"
};
db.AddOrUpdate(interval2);
var interval3 = new TimelineInterval
{
IntervalStart = 10,
IntervalStop = 12,
IntervalStart = baseIntervalValue + 10,
IntervalStop = baseIntervalValue + 12,
TimelineName = "Name1"
};
db.AddOrUpdate(interval3);
var interval4 = new TimelineInterval
{
IntervalStart = 0,
IntervalStop = 1,
IntervalStart = baseIntervalValue + 0,
IntervalStop = baseIntervalValue + 1,
TimelineName = "Name1"
};
db.AddOrUpdate(interval4);
var interval5 = new TimelineInterval
{
IntervalStart = 15,
IntervalStop = 17,
IntervalStart = baseIntervalValue + 15,
IntervalStop = baseIntervalValue + 17,
TimelineName = "Name1"
};
db.AddOrUpdate(interval5);
var results = db.FindByInterval(5, 11);
var results = db.FindByInterval(baseIntervalValue + 5, baseIntervalValue + 11);
Assert.Equal(3, results.Count);
Assert.True(results.Contains(interval1));
Assert.True(results.Contains(interval2));
......
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
......@@ -191,7 +192,7 @@ namespace MySocialPortalLibTest.Service
{
break;
}
resultsInterval = SimulateQuerySpan(requestInterval, maxInterval);
resultsInterval = SimulateQuerySpan(requestInterval, maxInterval, true, true);
service.UpdateRequestedInterval(resultsInterval);
PrintIntervals(db.FindByTimelineName(name, ServiceName));
}
......@@ -218,11 +219,17 @@ namespace MySocialPortalLibTest.Service
_testOutputHelper.WriteLine("");
}
private TimelineInterval SimulateQuerySpan(TimelineInterval requestInterval, ulong maxDelta = ulong.MaxValue, bool forward = true)
private TimelineInterval SimulateQuerySpan(TimelineInterval requestInterval, ulong maxDelta = ulong.MaxValue,
bool forward = true, bool simulateNone = false)
{
var newInterval = requestInterval.Copy();
var delta = requestInterval.IntervalStop - requestInterval.IntervalStart;
var actualDelta = delta > maxDelta ? maxDelta : delta;
if (simulateNone && new Random().NextDouble() > 0.5)
{
actualDelta = 0;
}
if (forward)
{
......
using System;
using System.Collections.Generic;
using System.Linq;
using MySocialPortalLib.Model;
using MySocialPortalLib.Util;
using Xunit;
namespace MySocialPortalLibTest.Util
{
public class PersonDbMergingUtilsTest
{
[Fact]
public void TestLabeledValueMergeSingle()
{
var values = new List<LabeledValue<string>>
{
new LabeledValue<string>("Label1", "Value1"),
new LabeledValue<string>("Label2", "Value2"),
};
var value3 = new LabeledValue<string>("Label3", "Value3");
values.AddIfNew(value3);
Assert.Equal(3, values.Count);
var newValue2 = new LabeledValue<string>("Label2", "NewValue2");
values.AddIfNew(newValue2);
Assert.Equal(3, values.Count);
Assert.False(newValue2.Equals(values[1]));
values[1].Value = "";
values.AddIfNew(newValue2);
Assert.Equal(3, values.Count);
Assert.True(newValue2.Equals(values[1]));
}
[Fact]
public void TestLabeledValueMergeMultiple()
{
var values1 = new List<LabeledValue<string>>
{
new LabeledValue<string>("Label1", "Value1"),
new LabeledValue<string>("Label2", "Value2"),
new LabeledValue<string>("Label3", "")
};
var values2 = new List<LabeledValue<string>>
{
new LabeledValue<string>("Label2", "NewValue2"),
new LabeledValue<string>("Label3", "NewValue3"),
new LabeledValue<string>("Label4", "NewValue4")
};
values1.AddIfNew(values2);
Assert.Equal(4, values1.Count);
Assert.Equal("Value2", values1[1].Value);
Assert.Equal("NewValue3", values1[2].Value);
}
[Fact]
public void TestSocialMediaAccountDataMerge()
{
var emptySma = new SocialMediaAccountData();
var fullSma = GenerateFullSocialMediaData();
var originalSma = fullSma.Copy();
fullSma.Merge(emptySma, false);
Assert.False(originalSma.AllEquals(fullSma));
fullSma.Active = originalSma.Active;
Assert.True(originalSma.AllEquals(fullSma));
emptySma.Merge(fullSma);
Assert.False(originalSma.AllEquals(emptySma));
emptySma.Id = fullSma.Id;
Assert.True(originalSma.AllEquals(emptySma));
var newKey = "Value1Key";
var fullSma2 = GenerateFullSocialMediaData();
fullSma.AdditionalProperties[newKey] = "Value1Fullsma1";
fullSma2.AdditionalProperties[newKey] = "Value1Fullsma2";
fullSma.Merge(fullSma2);
Assert.Equal(5, fullSma.AdditionalProperties.Count);
Assert.Equal("Value1Fullsma2", fullSma.AdditionalProperties[newKey]);
fullSma.Merge(fullSma2, true);
Assert.Equal(3, fullSma.AdditionalProperties.Count);
originalSma.AdditionalProperties.Keys.ToList().ForEach(k => Assert.DoesNotContain(k, fullSma.AdditionalProperties.Keys));
}
[Fact]
public void TestSocialMediaDictionaryMerge()
{
var socialDictionary1 = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData {SocialMediaSystemName = "Network1"}},
{"Network2", new SocialMediaAccountData {SocialMediaSystemName = "Network2"}}
};
var socialDictionary2 = new Dictionary<string, SocialMediaAccountData>
{
{"Network2", new SocialMediaAccountData {SocialMediaSystemName = "Network2"}},
{"Network3", new SocialMediaAccountData {SocialMediaSystemName = "Network3"}}
};
socialDictionary1.AddIfNew(socialDictionary2);
Assert.Equal(3, socialDictionary1.Count);
}
[Fact]
public void TestPersonsListMerge()
{
var personList1 = new List<Person>
{
new Person
{
Id = "1234",
Name = "Jane Smith",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData()}
}
},
new Person
{
Name = "John Gooding",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData{ProfileId = "johngooding", SocialMediaSystemName = "Network1"}},
{"Network3", new SocialMediaAccountData{ProfileId = "john_gooding", SocialMediaSystemName = "Network3"}}
}
},
new Person
{
Name = "Lisa Lopez",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData{ProfileId = "lisalopez", SocialMediaSystemName = "Network1"}},
}
}
};
var personList2 = new List<Person>
{
new Person
{
Id = "1234",
Name = "Jane A. Smith",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network2", new SocialMediaAccountData()}
}
},
new Person
{
Name = "JohnG",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData{ProfileId = "johngooding", SocialMediaSystemName = "Network1"}},
{"Network2", new SocialMediaAccountData{ProfileId = "johng", SocialMediaSystemName = "Network2"}},
{"Network3", new SocialMediaAccountData{ProfileId = "john_gooding", SocialMediaSystemName = "Network3"}}
}
},
new Person
{
Name = "Johnathan G. Gooding IV",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network3", new SocialMediaAccountData{ProfileId = "john_gooding", SocialMediaSystemName = "Network3"}},
{"Network4", new SocialMediaAccountData{ProfileId = "johng4", SocialMediaSystemName = "Network4"}}
}
},
new Person
{
Name = "Kathy Morningside",
SocialMediaAccounts = new Dictionary<string, SocialMediaAccountData>
{
{"Network1", new SocialMediaAccountData{ProfileId = "kathym", SocialMediaSystemName = "Network1"}},
}
}
};
var (newPeople, mergedPeople, unchangedPeople) = PersonDatabaseMergingUtils.Merge(personList1, personList2);
Assert.Equal(2, mergedPeople.Count);
Assert.Equal(personList1[0].Name, mergedPeople[0].Name);
Assert.Equal(2, mergedPeople[0].SocialMediaAccounts.Count);
Assert.Equal(personList1[1].Name, mergedPeople[1].Name);
Assert.Equal(4, mergedPeople[1].SocialMediaAccounts.Count);
Assert.Equal(1, newPeople.Count);
Assert.Equal(newPeople.First(), personList2.Last());
Assert.Equal(1, unchangedPeople.Count);