1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
29 using System.Collections.Concurrent;
30 using System.Collections.Generic;
31 using System.ComponentModel;
32 using System.Diagnostics;
34 using System.Linq.Expressions;
36 using System.Reflection;
38 using System.Text.RegularExpressions;
39 using System.Threading;
40 using System.Windows.Forms;
41 using System.Xml.Serialization;
45 public class PostClass : ICloneable
47 public struct StatusGeo : IEquatable<StatusGeo>
49 public double Longitude { get; }
50 public double Latitude { get; }
52 public StatusGeo(double longitude, double latitude)
54 this.Longitude = longitude;
55 this.Latitude = latitude;
58 public override int GetHashCode()
59 => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode();
61 public override bool Equals(object obj)
62 => obj is StatusGeo ? this.Equals((StatusGeo)obj) : false;
64 public bool Equals(StatusGeo other)
65 => this.Longitude == other.Longitude && this.Latitude == other.Longitude;
67 public static bool operator ==(StatusGeo left, StatusGeo right)
68 => left.Equals(right);
70 public static bool operator !=(StatusGeo left, StatusGeo right)
71 => !left.Equals(right);
73 public string Nickname { get; set; }
74 public string TextFromApi { get; set; }
75 public string ImageUrl { get; set; }
76 public string ScreenName { get; set; }
77 public DateTime CreatedAt { get; set; }
78 public long StatusId { get; set; }
80 public string Text { get; set; }
81 public bool IsRead { get; set; }
82 public bool IsReply { get; set; }
83 public bool IsExcludeReply { get; set; }
84 private bool _IsProtect;
85 public bool IsOwl { get; set; }
87 public string InReplyToUser { get; set; }
88 private long? _InReplyToStatusId;
89 public string Source { get; set; }
90 public Uri SourceUri { get; set; }
91 public List<string> ReplyToList { get; set; }
92 public bool IsMe { get; set; }
93 public bool IsDm { get; set; }
94 public long UserId { get; set; }
95 public bool FilterHit { get; set; }
96 public string RetweetedBy { get; set; }
97 public long? RetweetedId { get; set; }
98 private bool _IsDeleted = false;
99 private StatusGeo? _postGeo = null;
100 public int RetweetedCount { get; set; }
101 public long? RetweetedByUserId { get; set; }
102 public long? InReplyToUserId { get; set; }
103 public List<MediaInfo> Media { get; set; }
104 public long[] QuoteStatusIds { get; set; }
106 public int FavoritedCount { get; set; }
108 private States _states = States.None;
123 Media = new List<MediaInfo>();
124 ReplyToList = new List<string>();
125 QuoteStatusIds = new long[0];
128 public string TextSingleLine
132 return this.TextFromApi == null ? null : this.TextFromApi.Replace("\n", " ");
140 if (this.RetweetedId != null)
142 var post = this.RetweetSource;
154 if (this.RetweetedId != null)
156 var post = this.RetweetSource;
165 public bool IsProtect
175 _states = _states | States.Protect;
179 _states = _states & ~States.Protect;
194 _states = _states | States.Mark;
198 _states = _states & ~States.Mark;
203 public long? InReplyToStatusId
207 return _InReplyToStatusId;
213 _states = _states | States.Reply;
217 _states = _states & ~States.Reply;
219 _InReplyToStatusId = value;
223 public bool IsDeleted
233 this.InReplyToStatusId = null;
234 this.InReplyToUser = "";
235 this.InReplyToUserId = null;
236 this.IsReply = false;
237 this.ReplyToList = new List<string>();
238 this._states = States.None;
244 protected virtual PostClass RetweetSource
248 return TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value);
252 public StatusGeo? PostGeo
262 _states |= States.Geo;
266 _states &= ~States.Geo;
272 public int StateIndex
276 return (int)_states - 1;
281 public string SourceHtml
285 if (this.SourceUri == null)
286 return WebUtility.HtmlEncode(this.Source);
288 return string.Format("<a href=\"{0}\" rel=\"nofollow\">{1}</a>",
289 WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source));
294 /// このツイートが指定したユーザーによって削除可能であるかを判定します
296 /// <param name="selfUserId">ツイートを削除しようとするユーザーのID</param>
297 /// <returns>削除可能であれば true、そうでなければ false</returns>
298 public bool CanDeleteBy(long selfUserId)
300 // 自分が送った DM と自分に届いた DM のどちらも削除可能
304 // 自分のツイート or 他人に RT された自分のツイート
305 if (this.UserId == selfUserId)
309 if (this.RetweetedByUserId == selfUserId)
315 public PostClass ConvertToOriginalPost()
317 if (this.RetweetedId == null)
318 throw new InvalidOperationException();
320 var originalPost = this.Clone();
322 originalPost.StatusId = this.RetweetedId.Value;
323 originalPost.RetweetedId = null;
324 originalPost.RetweetedBy = "";
325 originalPost.RetweetedByUserId = null;
326 originalPost.RetweetedCount = 1;
331 public PostClass Clone()
333 var clone = (PostClass)this.MemberwiseClone();
334 clone.ReplyToList = new List<string>(this.ReplyToList);
335 clone.Media = new List<MediaInfo>(this.Media);
336 clone.QuoteStatusIds = this.QuoteStatusIds.ToArray();
341 object ICloneable.Clone()
344 public override bool Equals(object obj)
346 if (obj == null || this.GetType() != obj.GetType()) return false;
347 return this.Equals((PostClass)obj);
350 public bool Equals(PostClass other)
352 if (other == null) return false;
353 return (this.Nickname == other.Nickname) &&
354 (this.TextFromApi == other.TextFromApi) &&
355 (this.ImageUrl == other.ImageUrl) &&
356 (this.ScreenName == other.ScreenName) &&
357 (this.CreatedAt == other.CreatedAt) &&
358 (this.StatusId == other.StatusId) &&
359 (this.IsFav == other.IsFav) &&
360 (this.Text == other.Text) &&
361 (this.IsRead == other.IsRead) &&
362 (this.IsReply == other.IsReply) &&
363 (this.IsExcludeReply == other.IsExcludeReply) &&
364 (this.IsProtect == other.IsProtect) &&
365 (this.IsOwl == other.IsOwl) &&
366 (this.IsMark == other.IsMark) &&
367 (this.InReplyToUser == other.InReplyToUser) &&
368 (this.InReplyToStatusId == other.InReplyToStatusId) &&
369 (this.Source == other.Source) &&
370 (this.SourceUri == other.SourceUri) &&
371 (this.ReplyToList.SequenceEqual(other.ReplyToList)) &&
372 (this.IsMe == other.IsMe) &&
373 (this.IsDm == other.IsDm) &&
374 (this.UserId == other.UserId) &&
375 (this.FilterHit == other.FilterHit) &&
376 (this.RetweetedBy == other.RetweetedBy) &&
377 (this.RetweetedId == other.RetweetedId) &&
378 (this.IsDeleted == other.IsDeleted) &&
379 (this.InReplyToUserId == other.InReplyToUserId);
383 public override int GetHashCode()
385 return this.StatusId.GetHashCode();
389 public class MediaInfo
391 public string Url { get; }
392 public string VideoUrl { get; }
394 public MediaInfo(string url)
399 public MediaInfo(string url, string videoUrl)
402 this.VideoUrl = !string.IsNullOrEmpty(videoUrl) ? videoUrl : null;
405 public override bool Equals(object obj)
407 var info = obj as MediaInfo;
408 return info != null &&
409 info.Url == this.Url &&
410 info.VideoUrl == this.VideoUrl;
413 public override int GetHashCode()
415 return (this.Url == null ? 0 : this.Url.GetHashCode()) ^
416 (this.VideoUrl == null ? 0 : this.VideoUrl.GetHashCode());
419 public override string ToString()
425 public sealed class TabInformations
427 //個別タブの情報をDictionaryで保持
428 private Dictionary<string, TabClass> _tabs = new Dictionary<string, TabClass>();
429 private ConcurrentDictionary<long, PostClass> _statuses = new ConcurrentDictionary<long, PostClass>();
430 private Dictionary<long, PostClass> _retweets = new Dictionary<long, PostClass>();
431 private Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
432 private Stack<TabClass> _removedTab = new Stack<TabClass>();
434 public ISet<long> BlockIds = new HashSet<long>();
435 public ISet<long> MuteUserIds = new HashSet<long>();
438 //AddPost(複数回) -> DistributePosts -> SubmitUpdate
440 private ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
441 private ConcurrentQueue<long> deleteQueue = new ConcurrentQueue<long>();
443 /// <summary>通知サウンドを再生する優先順位</summary>
444 private Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
446 [MyCommon.TabUsageType.DirectMessage] = 100,
447 [MyCommon.TabUsageType.Mentions] = 90,
448 [MyCommon.TabUsageType.UserDefined] = 80,
449 [MyCommon.TabUsageType.Home] = 70,
450 [MyCommon.TabUsageType.Favorites] = 60,
454 private readonly object LockObj = new object();
456 private static TabInformations _instance = new TabInformations();
459 private List<ListElement> _lists = new List<ListElement>();
461 private TabInformations()
465 public static TabInformations GetInstance()
467 return _instance; //singleton
470 public List<ListElement> SubscribableLists
478 if (value != null && value.Count > 0)
480 foreach (var tb in this.GetTabsByType(MyCommon.TabUsageType.Lists))
482 foreach (var list in value)
484 if (tb.ListInfo.Id == list.Id)
496 public bool AddTab(string TabName, MyCommon.TabUsageType TabType, ListElement List)
498 if (_tabs.ContainsKey(TabName)) return false;
499 var tb = new TabClass(TabName, TabType, List);
500 _tabs.Add(TabName, tb);
501 tb.SetSortMode(this.SortMode, this.SortOrder);
505 //public void AddTab(string TabName, TabClass Tab)
507 // _tabs.Add(TabName, Tab);
510 public void RemoveTab(string TabName)
514 var tb = GetTabByName(TabName);
515 if (tb.IsDefaultTabType) return; //念のため
517 if (!tb.IsInnerStorageTabType)
519 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
520 var dmTab = GetTabByType(MyCommon.TabUsageType.DirectMessage);
522 for (int idx = 0; idx < tb.AllCount; ++idx)
525 var Id = tb.GetId(idx);
526 if (Id < 0) continue;
527 foreach (var tab in _tabs.Values)
529 if (tab != tb && tab != dmTab)
531 if (tab.Contains(Id))
538 if (!exist) homeTab.AddPostImmediately(Id, _statuses[Id].IsRead);
541 _removedTab.Push(tb);
542 _tabs.Remove(TabName);
546 public Stack<TabClass> RemovedTab
548 get { return _removedTab; }
551 public bool ContainsTab(string TabText)
553 return _tabs.ContainsKey(TabText);
556 public bool ContainsTab(TabClass ts)
558 return _tabs.ContainsValue(ts);
562 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
564 /// <param name="baseTabName">作成したいタブ名</param>
565 /// <returns>生成されたタブ名</returns>
566 /// <exception cref="TabException">タブ名の生成を 100 回試行して失敗した場合</exception>
567 public string MakeTabName(string baseTabName)
569 return this.MakeTabName(baseTabName, 100);
573 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
575 /// <param name="baseTabName">作成したいタブ名</param>
576 /// <param name="retryCount">重複を避けたタブ名を生成する試行回数</param>
577 /// <returns>生成されたタブ名</returns>
578 /// <exception cref="TabException">retryCount で指定された回数だけタブ名の生成を試行して失敗した場合</exception>
579 public string MakeTabName(string baseTabName, int retryCount)
581 if (!this.ContainsTab(baseTabName))
584 foreach (var i in Enumerable.Range(2, retryCount - 1))
586 var tabName = baseTabName + i;
587 if (!this.ContainsTab(tabName))
593 var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
594 throw new TabException(message);
597 public Dictionary<string, TabClass> Tabs
609 public Dictionary<string, TabClass>.KeyCollection KeysTab
617 public SortOrder SortOrder { get; private set; }
619 public ComparerMode SortMode { get; private set; }
621 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
623 this.SortMode = mode;
624 this.SortOrder = sortOrder;
626 foreach (var tab in this._tabs.Values)
627 tab.SetSortMode(mode, sortOrder);
630 public SortOrder ToggleSortOrder(ComparerMode sortMode)
632 var sortOrder = this.SortOrder;
634 if (this.SortMode == sortMode)
636 if (sortOrder == SortOrder.Ascending)
637 sortOrder = SortOrder.Descending;
639 sortOrder = SortOrder.Ascending;
643 sortOrder = SortOrder.Ascending;
646 this.SetSortMode(sortMode, sortOrder);
648 return this.SortOrder;
651 // public PostClass RetweetSource(long Id)
655 // if (_retweets.ContainsKey(Id))
657 // return _retweets[Id];
665 public PostClass RetweetSource(long Id)
668 return _retweets.TryGetValue(Id, out status)
673 public void RemoveFavPost(long Id)
678 var tab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
680 if (_statuses.TryGetValue(Id, out post))
683 var tType = tab.TabType;
684 if (tab.Contains(Id))
687 //FavタブからRetweet発言を削除する場合は、他の同一参照Retweetも削除
688 if (tType == MyCommon.TabUsageType.Favorites && post.RetweetedId != null)
690 for (int i = 0; i < tab.AllCount; i++)
692 PostClass rPost = null;
697 catch (ArgumentOutOfRangeException)
701 if (rPost.RetweetedId != null && rPost.RetweetedId == post.RetweetedId)
703 tab.Remove(rPost.StatusId);
708 //TabType=PublicSearchの場合(Postの保存先がTabClass内)
709 //if (tab.Contains(StatusId) &&
710 // (tab.TabType = MyCommon.TabUsageType.PublicSearch || tab.TabType = MyCommon.TabUsageType.DirectMessage))
712 // tab.Remove(StatusId);
717 public void ScrubGeoReserve(long id, long upToStatusId)
721 //this._scrubGeo.Add(new ScrubGeoInfo With {.UserId = id, .UpToStatusId = upToStatusId});
722 this.ScrubGeo(id, upToStatusId);
726 private void ScrubGeo(long userId, long upToStatusId)
730 var userPosts = from post in this._statuses.Values
731 where post.UserId == userId && post.UserId <= upToStatusId
734 foreach (var p in userPosts)
739 var userPosts2 = from tb in this.GetTabsInnerStorageType()
740 from post in tb.Posts.Values
741 where post.UserId == userId && post.UserId <= upToStatusId
744 foreach (var p in userPosts2)
751 public void RemovePostReserve(long id)
755 this.deleteQueue.Enqueue(id);
756 this.DeletePost(id); //UI選択行がずれるため、RemovePostは使用しない
760 public void RemovePost(long Id)
765 foreach (var tab in _tabs.Values)
767 if (tab.Contains(Id))
771 PostClass removedPost;
772 _statuses.TryRemove(Id, out removedPost);
776 private void DeletePost(long Id)
781 if (_statuses.TryGetValue(Id, out post))
783 post.IsDeleted = true;
785 foreach (var tb in this.GetTabsInnerStorageType())
790 post.IsDeleted = true;
796 public int SubmitUpdate()
799 PostClass[] notifyPosts;
800 bool newMentionOrDm, isDeletePost;
802 return this.SubmitUpdate(out soundFile, out notifyPosts, out newMentionOrDm,
806 public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
807 out bool newMentionOrDm, out bool isDeletePost)
813 notifyPosts = new PostClass[0];
814 newMentionOrDm = false;
815 isDeletePost = false;
818 var notifyPostsList = new List<PostClass>();
820 var currentNotifyPriority = -1;
822 foreach (var tab in this._tabs.Values)
825 var addedIds = tab.AddSubmit();
827 if (tab.TabType == MyCommon.TabUsageType.Mentions ||
828 tab.TabType == MyCommon.TabUsageType.DirectMessage)
830 if (addedIds.Count > 0)
831 newMentionOrDm = true;
834 if (addedIds.Count != 0 && tab.Notify)
837 foreach (var statusId in addedIds)
840 if (tab.Posts.TryGetValue(statusId, out post))
841 notifyPostsList.Add(post);
845 if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out notifyPriority))
848 if (notifyPriority > currentNotifyPriority)
851 soundFile = tab.SoundFile;
852 currentNotifyPriority = notifyPriority;
856 totalPosts += addedIds.Count;
859 notifyPosts = notifyPostsList.Distinct().ToArray();
861 long deletedStatusId;
862 while (this.deleteQueue.TryDequeue(out deletedStatusId))
864 this.RemovePost(deletedStatusId);
872 public int DistributePosts()
876 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
877 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
878 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
880 var distributableTabs = this._tabs.Values.Where(x => x.IsDistributableTabType)
886 while (this.addQueue.TryDequeue(out statusId))
889 if (!this._statuses.TryGetValue(statusId, out post))
892 var filterHit = false; // フィルタにヒットしたタブがあるか
893 var mark = false; // フィルタによってマーク付けされたか
894 var excludedReply = false; // リプライから除外されたか
895 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
897 foreach (var tab in distributableTabs)
900 switch (tab.AddFiltered(post))
902 case MyCommon.HITRESULT.Copy:
905 case MyCommon.HITRESULT.CopyAndMark:
909 case MyCommon.HITRESULT.Move:
913 case MyCommon.HITRESULT.None:
915 case MyCommon.HITRESULT.Exclude:
916 if (tab.TabType == MyCommon.TabUsageType.Mentions)
917 excludedReply = true;
922 post.FilterHit = filterHit;
924 post.IsExcludeReply = excludedReply;
926 // 移動されなかったらRecentに追加
928 homeTab.AddPostQueue(post.StatusId, post.IsRead);
930 // 除外ルール適用のないReplyならReplyタブに追加
931 if (post.IsReply && !excludedReply)
932 replyTab.AddPostQueue(post.StatusId, post.IsRead);
934 // Fav済み発言だったらFavoritesタブに追加
936 favTab.AddPostQueue(post.StatusId, post.IsRead);
945 public void AddPost(PostClass Item)
947 Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
951 if (this.IsMuted(Item))
955 if (_statuses.TryGetValue(Item.StatusId, out status))
959 if (Item.RetweetedId == null)
970 return; //追加済みなら何もしない
975 if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
977 if (SettingCommon.Instance.HideDuplicatedRetweets &&
979 Item.RetweetedId != null &&
980 this._retweets.TryGetValue(Item.RetweetedId.Value, out status) &&
981 status.RetweetedCount > 0) return;
983 if (BlockIds.Contains(Item.UserId))
986 _statuses.TryAdd(Item.StatusId, Item);
988 if (Item.RetweetedId != null)
990 this.AddRetweet(Item);
992 if (Item.IsFav && _retweets.ContainsKey(Item.StatusId))
994 return; //Fav済みのRetweet元発言は追加しない
996 this.addQueue.Enqueue(Item.StatusId);
1000 public bool IsMuted(PostClass post)
1002 var muteTab = this.GetTabByType(MyCommon.TabUsageType.Mute);
1003 if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
1006 // これ以降は Twitter 標準のミュート機能に準じた判定
1009 // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
1013 if (this.MuteUserIds.Contains(post.UserId))
1016 if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
1022 private void AddRetweet(PostClass item)
1024 var retweetedId = item.RetweetedId.Value;
1027 if (this._retweets.TryGetValue(retweetedId, out status))
1029 status.RetweetedCount++;
1030 if (status.RetweetedCount > 10)
1032 status.RetweetedCount = 0;
1037 this._retweets.Add(retweetedId, item.ConvertToOriginalPost());
1040 public bool AddQuoteTweet(PostClass item)
1044 if (IsMuted(item) || BlockIds.Contains(item.UserId))
1047 _quotes[item.StatusId] = item;
1053 /// 全てのタブを横断して既読状態を変更します
1055 /// <param name="statusId">変更するツイートのID</param>
1056 /// <param name="read">既読状態</param>
1057 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1058 public bool SetReadAllTab(long statusId, bool read)
1062 foreach (var tab in this._tabs.Values)
1064 if (!tab.Contains(statusId))
1067 tab.SetReadState(statusId, read);
1070 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
1072 if (this.Posts.TryGetValue(statusId, out post))
1080 /// Home タブのツイートを全て既読にします。
1081 /// ただし IsReply または FilterHit が true なものを除きます。
1083 public void SetReadHomeTab()
1085 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
1089 foreach (var statusId in homeTab.GetUnreadIds())
1092 if (!this.Posts.TryGetValue(statusId, out post))
1095 if (post.IsReply || post.FilterHit)
1098 this.SetReadAllTab(post.StatusId, read: true);
1103 public PostClass this[long ID]
1108 if (this._statuses.TryGetValue(ID, out status))
1111 if (this._retweets.TryGetValue(ID, out status))
1114 if (this._quotes.TryGetValue(ID, out status))
1117 return this.GetTabsInnerStorageType()
1118 .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
1119 .Where(x => x != null)
1124 public bool ContainsKey(long Id)
1129 return _statuses.ContainsKey(Id);
1133 public void RenameTab(string Original, string NewName)
1135 var tb = _tabs[Original];
1136 _tabs.Remove(Original);
1137 tb.TabName = NewName;
1138 _tabs.Add(NewName, tb);
1141 public void FilterAll()
1145 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
1146 var detachedIdsAll = Enumerable.Empty<long>();
1148 foreach (var tab in _tabs.Values.ToArray())
1150 if (!tab.IsDistributableTabType)
1153 if (tab.TabType == MyCommon.TabUsageType.Mute)
1156 // フィルタに変更のあったタブのみを対象とする
1157 if (!tab.FilterModified)
1160 tab.FilterModified = false;
1162 // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
1163 var orgIds = tab.BackupIds;
1166 foreach (var post in _statuses.Values)
1168 var filterHit = false; // フィルタにヒットしたタブがあるか
1169 var mark = false; // フィルタによってマーク付けされたか
1170 var excluded = false; // 除外フィルタによって除外されたか
1171 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
1173 switch (tab.AddFiltered(post, immediately: true))
1175 case MyCommon.HITRESULT.Copy:
1178 case MyCommon.HITRESULT.CopyAndMark:
1182 case MyCommon.HITRESULT.Move:
1186 case MyCommon.HITRESULT.None:
1188 case MyCommon.HITRESULT.Exclude:
1193 post.FilterHit = filterHit;
1198 homeTab.Remove(post.StatusId);
1200 if (tab.TabType == MyCommon.TabUsageType.Mentions)
1202 post.IsExcludeReply = excluded;
1204 // 除外ルール適用のないReplyならReplyタブに追加
1205 if (post.IsReply && !excluded)
1206 tab.AddPostImmediately(post.StatusId, post.IsRead);
1210 // フィルタの更新によってタブから取り除かれたツイートのID
1211 var detachedIds = orgIds.Except(tab.BackupIds).ToArray();
1213 detachedIdsAll = detachedIdsAll.Concat(detachedIds);
1216 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
1217 foreach (var id in detachedIdsAll)
1220 foreach (var tbTemp in _tabs.Values.ToArray())
1222 if (!tbTemp.IsDistributableTabType)
1225 if (tbTemp.Contains(id))
1235 if (this._statuses.TryGetValue(id, out post))
1236 homeTab.AddPostImmediately(post.StatusId, post.IsRead);
1242 public void ClearTabIds(string TabName)
1247 var tb = _tabs[TabName];
1248 if (!tb.IsInnerStorageTabType)
1250 foreach (var Id in tb.BackupIds)
1253 foreach (var tab in _tabs.Values)
1255 if (tab.Contains(Id))
1263 PostClass removedPost;
1264 _statuses.TryRemove(Id, out removedPost);
1274 public void RefreshOwl(ISet<long> follower)
1278 if (follower.Count > 0)
1280 foreach (var post in _statuses.Values)
1282 //if (post.UserId = 0 || post.IsDm) Continue For
1289 post.IsOwl = !follower.Contains(post.UserId);
1295 foreach (var post in _statuses.Values)
1303 public TabClass GetTabByType(MyCommon.TabUsageType tabType)
1305 //Home,Mentions,DM,Favは1つに制限する
1306 //その他のタイプを指定されたら、最初に合致したものを返す
1310 return this._tabs.Values
1311 .FirstOrDefault(x => x.TabType.HasFlag(tabType));
1315 public TabClass[] GetTabsByType(MyCommon.TabUsageType tabType)
1319 return this._tabs.Values
1320 .Where(x => x.TabType.HasFlag(tabType))
1325 public TabClass[] GetTabsInnerStorageType()
1329 return this._tabs.Values
1330 .Where(x => x.IsInnerStorageTabType)
1335 public TabClass GetTabByName(string tabName)
1340 return _tabs.TryGetValue(tabName, out tab)
1346 public ConcurrentDictionary<long, PostClass> Posts
1351 public sealed class TabClass
1353 private List<PostFilterRule> _filters;
1354 private IndexedSortedSet<long> _ids;
1355 private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
1356 private SortedSet<long> unreadIds = new SortedSet<long>();
1357 private MyCommon.TabUsageType _tabType = MyCommon.TabUsageType.Undefined;
1359 private readonly object _lockObj = new object();
1361 public string User { get; set; }
1365 private string _searchLang = "";
1366 private string _searchWords = "";
1368 public string SearchLang
1376 _searchLang = value;
1377 this.ResetFetchIds();
1380 public string SearchWords
1384 return _searchWords;
1388 _searchWords = value.Trim();
1389 this.ResetFetchIds();
1393 private Dictionary<string, string> _beforeQuery = new Dictionary<string, string>();
1395 public bool IsSearchQueryChanged
1399 var qry = new Dictionary<string, string>();
1400 if (!string.IsNullOrEmpty(_searchWords))
1402 qry.Add("q", _searchWords);
1403 if (!string.IsNullOrEmpty(_searchLang)) qry.Add("lang", _searchLang);
1405 if (qry.Count != _beforeQuery.Count)
1411 foreach (var kvp in qry)
1414 if (!_beforeQuery.TryGetValue(kvp.Key, out value) || value != kvp.Value)
1427 private ListElement _listInfo;
1428 public ListElement ListInfo
1442 public PostClass RelationTargetPost { get; set; }
1445 public long OldestId = long.MaxValue;
1448 public long SinceId { get; set; }
1451 public ConcurrentDictionary<long, PostClass> Posts { get; private set; }
1453 private ConcurrentDictionary<long, PostClass> _innerPosts;
1455 private struct TemporaryId
1460 public TemporaryId(long argId, bool argRead)
1469 _innerPosts = new ConcurrentDictionary<long, PostClass>();
1470 Posts = _innerPosts;
1473 _filters = new List<PostFilterRule>();
1477 UnreadManage = true;
1478 _ids = new IndexedSortedSet<long>();
1479 _tabType = MyCommon.TabUsageType.Undefined;
1483 public TabClass(string TabName, MyCommon.TabUsageType TabType, ListElement list) : this()
1485 this.TabName = TabName;
1486 this.TabType = TabType;
1487 this.ListInfo = list;
1491 /// タブ更新時に使用する SinceId, OldestId をリセットする
1493 public void ResetFetchIds()
1496 this.OldestId = long.MaxValue;
1500 /// ソート対象のフィールドとソート順を設定し、ソートを実行します
1502 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
1504 this.SortMode = mode;
1505 this.SortOrder = sortOrder;
1507 this.ApplySortMode();
1510 private void ApplySortMode()
1512 var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
1514 Comparison<long> comparison;
1515 if (this.SortMode == ComparerMode.Id)
1517 comparison = (x, y) => sign * x.CompareTo(y);
1521 Comparison<PostClass> postComparison;
1522 switch (this.SortMode)
1524 case ComparerMode.Data:
1525 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
1527 case ComparerMode.Name:
1528 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
1530 case ComparerMode.Nickname:
1531 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
1533 case ComparerMode.Source:
1534 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
1537 throw new InvalidEnumArgumentException();
1540 comparison = (x, y) =>
1542 PostClass xPost, yPost;
1543 this.Posts.TryGetValue(x, out xPost);
1544 this.Posts.TryGetValue(y, out yPost);
1546 var compare = sign * postComparison(xPost, yPost);
1550 // 同値であれば status_id で比較する
1551 return sign * x.CompareTo(y);
1555 var comparer = Comparer<long>.Create(comparison);
1557 this._ids = new IndexedSortedSet<long>(this._ids, comparer);
1558 this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
1562 public ComparerMode SortMode { get; private set; }
1565 public SortOrder SortOrder { get; private set; }
1567 public void AddPostQueue(long statusId, bool read)
1569 this.addQueue.Enqueue(new TemporaryId(statusId, read));
1573 public void AddPostImmediately(long ID, bool Read)
1575 if (this._ids.Contains(ID)) return;
1580 this.unreadIds.Add(ID);
1584 public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false)
1586 if (this.IsInnerStorageTabType) return MyCommon.HITRESULT.None;
1588 var rslt = MyCommon.HITRESULT.None;
1590 lock (this._lockObj)
1592 foreach (var ft in _filters)
1596 switch (ft.ExecFilter(post)) //フィルタクラスでヒット判定
1598 case MyCommon.HITRESULT.None:
1600 case MyCommon.HITRESULT.Copy:
1601 if (rslt != MyCommon.HITRESULT.CopyAndMark) rslt = MyCommon.HITRESULT.Copy;
1603 case MyCommon.HITRESULT.CopyAndMark:
1604 rslt = MyCommon.HITRESULT.CopyAndMark;
1606 case MyCommon.HITRESULT.Move:
1607 rslt = MyCommon.HITRESULT.Move;
1609 case MyCommon.HITRESULT.Exclude:
1610 rslt = MyCommon.HITRESULT.Exclude;
1614 catch (NullReferenceException)
1616 // ExecFilterでNullRef出る場合あり。暫定対応
1617 MyCommon.TraceOut("ExecFilterでNullRef: " + ft.ToString());
1618 rslt = MyCommon.HITRESULT.None;
1625 if (this.TabType != MyCommon.TabUsageType.Mute &&
1626 rslt != MyCommon.HITRESULT.None && rslt != MyCommon.HITRESULT.Exclude)
1629 this.AddPostImmediately(post.StatusId, post.IsRead);
1631 this.AddPostQueue(post.StatusId, post.IsRead);
1634 return rslt; //マーク付けは呼び出し元で行うこと
1638 public void AddPostToInnerStorage(PostClass Post)
1640 if (_innerPosts.ContainsKey(Post.StatusId)) return;
1641 _innerPosts.TryAdd(Post.StatusId, Post);
1642 this.AddPostQueue(Post.StatusId, Post.IsRead);
1645 public IList<long> AddSubmit()
1647 var addedIds = new List<long>();
1650 while (this.addQueue.TryDequeue(out tId))
1652 this.AddPostImmediately(tId.Id, tId.Read);
1653 addedIds.Add(tId.Id);
1659 public void Remove(long Id)
1661 if (!this._ids.Contains(Id))
1664 this._ids.Remove(Id);
1665 this.unreadIds.Remove(Id);
1667 if (this.IsInnerStorageTabType)
1669 PostClass removedPost;
1670 this._innerPosts.TryRemove(Id, out removedPost);
1674 public bool UnreadManage { get; set; }
1676 // v1.0.5で「タブを固定(Locked)」から「タブを保護(Protected)」に名称変更
1677 [XmlElement(ElementName = "Locked")]
1678 public bool Protected { get; set; }
1680 public bool Notify { get; set; }
1682 public string SoundFile { get; set; }
1685 /// 次に表示する未読ツイートのIDを返します。
1686 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1689 public long NextUnreadId
1693 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1696 if (this.unreadIds.Count == 0)
1699 // unreadIds はリストのインデックス番号順に並んでいるため、
1700 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
1701 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
1706 /// 次に表示する未読ツイートのインデックス番号を返します。
1707 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1710 public int NextUnreadIndex
1714 var unreadId = this.NextUnreadId;
1715 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
1721 /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
1724 public int UnreadCount
1728 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1731 return this.unreadIds.Count;
1739 return this._ids.Count;
1744 /// 未読ツイートの ID を配列で返します
1746 public long[] GetUnreadIds()
1748 lock (this._lockObj)
1750 return this.unreadIds.ToArray();
1758 /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
1760 /// <param name="statusId">変更するツイートのID</param>
1761 /// <param name="read">既読状態</param>
1762 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1763 internal bool SetReadState(long statusId, bool read)
1765 if (!this._ids.Contains(statusId))
1766 throw new ArgumentException(nameof(statusId));
1768 if (this.IsInnerStorageTabType)
1769 this.Posts[statusId].IsRead = read;
1772 return this.unreadIds.Remove(statusId);
1774 return this.unreadIds.Add(statusId);
1777 public PostFilterRule[] GetFilters()
1779 lock (this._lockObj)
1781 return _filters.ToArray();
1785 public void RemoveFilter(PostFilterRule filter)
1787 lock (this._lockObj)
1789 _filters.Remove(filter);
1790 filter.PropertyChanged -= this.OnFilterModified;
1791 this.FilterModified = true;
1795 public bool AddFilter(PostFilterRule filter)
1797 lock (this._lockObj)
1799 if (_filters.Contains(filter)) return false;
1800 filter.PropertyChanged += this.OnFilterModified;
1801 _filters.Add(filter);
1802 this.FilterModified = true;
1807 private void OnFilterModified(object sender, PropertyChangedEventArgs e)
1809 this.FilterModified = true;
1812 public PostFilterRule[] FilterArray
1816 lock (this._lockObj)
1818 return _filters.ToArray();
1823 lock (this._lockObj)
1825 foreach (var oldFilter in this._filters)
1827 oldFilter.PropertyChanged -= this.OnFilterModified;
1830 this._filters.Clear();
1831 this.FilterModified = true;
1833 foreach (var newFilter in value)
1835 _filters.Add(newFilter);
1836 newFilter.PropertyChanged += this.OnFilterModified;
1841 public bool Contains(long ID)
1843 return _ids.Contains(ID);
1846 public void ClearIDs()
1849 this.unreadIds.Clear();
1850 _innerPosts.Clear();
1852 Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
1855 public PostClass this[int Index]
1859 var id = GetId(Index);
1860 if (id < 0) throw new ArgumentException("Index can't find. Index=" + Index.ToString() + "/TabName=" + TabName, nameof(Index));
1865 public PostClass[] this[int StartIndex, int EndIndex]
1869 var length = EndIndex - StartIndex + 1;
1870 var posts = new PostClass[length];
1871 for (int i = 0; i < length; i++)
1873 posts[i] = Posts[GetId(StartIndex + i)];
1879 public long[] GetId(ListView.SelectedIndexCollection IndexCollection)
1881 if (IndexCollection.Count == 0) return null;
1883 var Ids = new long[IndexCollection.Count];
1884 for (int i = 0; i < Ids.Length; i++)
1886 Ids[i] = GetId(IndexCollection[i]);
1891 public long GetId(int Index)
1893 return Index < _ids.Count ? _ids[Index] : -1;
1896 public int[] IndexOf(long[] Ids)
1898 if (Ids == null) return null;
1899 var idx = new int[Ids.Length];
1900 for (int i = 0; i < Ids.Length; i++)
1902 idx[i] = IndexOf(Ids[i]);
1907 public int IndexOf(long ID)
1909 return _ids.IndexOf(ID);
1913 public bool FilterModified { get; set; }
1915 public long[] BackupIds
1919 return _ids.ToArray();
1923 public string TabName { get; set; }
1925 public MyCommon.TabUsageType TabType
1934 if (this.IsInnerStorageTabType)
1936 Posts = _innerPosts;
1940 Posts = TabInformations.GetInstance().Posts;
1945 public bool IsDefaultTabType
1949 return _tabType.IsDefault();
1953 public bool IsDistributableTabType
1957 return _tabType.IsDistributable();
1961 public bool IsInnerStorageTabType
1965 return _tabType.IsInnerStorage();
1971 /// enum TabUsageType に対応する拡張メソッドを定義したクラス
1973 public static class TabUsageTypeExt
1975 const MyCommon.TabUsageType DefaultTabTypeMask =
1976 MyCommon.TabUsageType.Home |
1977 MyCommon.TabUsageType.Mentions |
1978 MyCommon.TabUsageType.DirectMessage |
1979 MyCommon.TabUsageType.Favorites |
1980 MyCommon.TabUsageType.Mute;
1982 const MyCommon.TabUsageType DistributableTabTypeMask =
1983 MyCommon.TabUsageType.Mentions |
1984 MyCommon.TabUsageType.UserDefined |
1985 MyCommon.TabUsageType.Mute;
1987 const MyCommon.TabUsageType InnerStorageTabTypeMask =
1988 MyCommon.TabUsageType.DirectMessage |
1989 MyCommon.TabUsageType.PublicSearch |
1990 MyCommon.TabUsageType.Lists |
1991 MyCommon.TabUsageType.UserTimeline |
1992 MyCommon.TabUsageType.Related |
1993 MyCommon.TabUsageType.SearchResults;
1996 /// デフォルトタブかどうかを示す値を取得します。
1998 public static bool IsDefault(this MyCommon.TabUsageType tabType)
2000 return (tabType & DefaultTabTypeMask) != 0;
2004 /// 振り分け可能タブかどうかを示す値を取得します。
2006 public static bool IsDistributable(this MyCommon.TabUsageType tabType)
2008 return (tabType & DistributableTabTypeMask) != 0;
2012 /// 内部ストレージを使用するタブかどうかを示す値を取得します。
2014 public static bool IsInnerStorage(this MyCommon.TabUsageType tabType)
2016 return (tabType & InnerStorageTabTypeMask) != 0;
2023 public enum ComparerMode