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(bool isUserStream = false)
799 PostClass[] notifyPosts;
800 bool isMentionIncluded, isDeletePost;
802 return this.SubmitUpdate(out soundFile, out notifyPosts, out isMentionIncluded,
803 out isDeletePost, isUserStream);
806 public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
807 out bool isMentionIncluded, out bool isDeletePost, bool isUserStream)
813 notifyPosts = new PostClass[0];
814 isMentionIncluded = 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)
829 if (addedIds.Select(x => tab.Posts[x]).Any(x => x.IsReply))
830 isMentionIncluded = true;
833 if (addedIds.Count != 0 && tab.Notify)
836 foreach (var statusId in addedIds)
839 if (tab.Posts.TryGetValue(statusId, out post))
840 notifyPostsList.Add(post);
844 if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out notifyPriority))
847 if (notifyPriority > currentNotifyPriority)
850 soundFile = tab.SoundFile;
851 currentNotifyPriority = notifyPriority;
855 totalPosts += addedIds.Count;
858 notifyPosts = notifyPostsList.Distinct().ToArray();
863 while (this.deleteQueue.TryDequeue(out statusId))
865 this.RemovePost(statusId);
874 public int DistributePosts()
878 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
879 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
880 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
882 var distributableTabs = this._tabs.Values.Where(x => x.IsDistributableTabType)
888 while (this.addQueue.TryDequeue(out statusId))
891 if (!this._statuses.TryGetValue(statusId, out post))
894 var filterHit = false; // フィルタにヒットしたタブがあるか
895 var mark = false; // フィルタによってマーク付けされたか
896 var excludedReply = false; // リプライから除外されたか
897 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
899 foreach (var tab in distributableTabs)
902 switch (tab.AddFiltered(post))
904 case MyCommon.HITRESULT.Copy:
907 case MyCommon.HITRESULT.CopyAndMark:
911 case MyCommon.HITRESULT.Move:
915 case MyCommon.HITRESULT.None:
917 case MyCommon.HITRESULT.Exclude:
918 if (tab.TabType == MyCommon.TabUsageType.Mentions)
919 excludedReply = true;
924 post.FilterHit = filterHit;
926 post.IsExcludeReply = excludedReply;
928 // 移動されなかったらRecentに追加
930 homeTab.AddPostQueue(post.StatusId, post.IsRead);
932 // 除外ルール適用のないReplyならReplyタブに追加
933 if (post.IsReply && !excludedReply)
934 replyTab.AddPostQueue(post.StatusId, post.IsRead);
936 // Fav済み発言だったらFavoritesタブに追加
938 favTab.AddPostQueue(post.StatusId, post.IsRead);
947 public void AddPost(PostClass Item)
949 Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
953 if (this.IsMuted(Item))
957 if (_statuses.TryGetValue(Item.StatusId, out status))
961 if (Item.RetweetedId == null)
972 return; //追加済みなら何もしない
977 if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
979 if (SettingCommon.Instance.HideDuplicatedRetweets &&
981 Item.RetweetedId != null &&
982 this._retweets.TryGetValue(Item.RetweetedId.Value, out status) &&
983 status.RetweetedCount > 0) return;
985 if (BlockIds.Contains(Item.UserId))
988 _statuses.TryAdd(Item.StatusId, Item);
990 if (Item.RetweetedId != null)
992 this.AddRetweet(Item);
994 if (Item.IsFav && _retweets.ContainsKey(Item.StatusId))
996 return; //Fav済みのRetweet元発言は追加しない
998 this.addQueue.Enqueue(Item.StatusId);
1002 public bool IsMuted(PostClass post)
1004 var muteTab = this.GetTabByType(MyCommon.TabUsageType.Mute);
1005 if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
1008 // これ以降は Twitter 標準のミュート機能に準じた判定
1011 // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
1015 if (this.MuteUserIds.Contains(post.UserId))
1018 if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
1024 private void AddRetweet(PostClass item)
1026 var retweetedId = item.RetweetedId.Value;
1029 if (this._retweets.TryGetValue(retweetedId, out status))
1031 status.RetweetedCount++;
1032 if (status.RetweetedCount > 10)
1034 status.RetweetedCount = 0;
1039 this._retweets.Add(retweetedId, item.ConvertToOriginalPost());
1042 public bool AddQuoteTweet(PostClass item)
1046 if (IsMuted(item) || BlockIds.Contains(item.UserId))
1049 _quotes[item.StatusId] = item;
1055 /// 全てのタブを横断して既読状態を変更します
1057 /// <param name="statusId">変更するツイートのID</param>
1058 /// <param name="read">既読状態</param>
1059 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1060 public bool SetReadAllTab(long statusId, bool read)
1064 foreach (var tab in this._tabs.Values)
1066 if (!tab.Contains(statusId))
1069 tab.SetReadState(statusId, read);
1072 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
1074 if (this.Posts.TryGetValue(statusId, out post))
1082 /// Home タブのツイートを全て既読にします。
1083 /// ただし IsReply または FilterHit が true なものを除きます。
1085 public void SetReadHomeTab()
1087 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
1091 foreach (var statusId in homeTab.GetUnreadIds())
1094 if (!this.Posts.TryGetValue(statusId, out post))
1097 if (post.IsReply || post.FilterHit)
1100 this.SetReadAllTab(post.StatusId, read: true);
1105 public PostClass this[long ID]
1110 if (this._statuses.TryGetValue(ID, out status))
1113 if (this._retweets.TryGetValue(ID, out status))
1116 if (this._quotes.TryGetValue(ID, out status))
1119 return this.GetTabsInnerStorageType()
1120 .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
1121 .Where(x => x != null)
1126 public bool ContainsKey(long Id)
1131 return _statuses.ContainsKey(Id);
1135 public void RenameTab(string Original, string NewName)
1137 var tb = _tabs[Original];
1138 _tabs.Remove(Original);
1139 tb.TabName = NewName;
1140 _tabs.Add(NewName, tb);
1143 public void FilterAll()
1147 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
1148 var detachedIdsAll = Enumerable.Empty<long>();
1150 foreach (var tab in _tabs.Values.ToArray())
1152 if (!tab.IsDistributableTabType)
1155 if (tab.TabType == MyCommon.TabUsageType.Mute)
1158 // フィルタに変更のあったタブのみを対象とする
1159 if (!tab.FilterModified)
1162 tab.FilterModified = false;
1164 // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
1165 var orgIds = tab.BackupIds;
1168 foreach (var post in _statuses.Values)
1170 var filterHit = false; // フィルタにヒットしたタブがあるか
1171 var mark = false; // フィルタによってマーク付けされたか
1172 var excluded = false; // 除外フィルタによって除外されたか
1173 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
1175 switch (tab.AddFiltered(post, immediately: true))
1177 case MyCommon.HITRESULT.Copy:
1180 case MyCommon.HITRESULT.CopyAndMark:
1184 case MyCommon.HITRESULT.Move:
1188 case MyCommon.HITRESULT.None:
1190 case MyCommon.HITRESULT.Exclude:
1195 post.FilterHit = filterHit;
1200 homeTab.Remove(post.StatusId);
1202 if (tab.TabType == MyCommon.TabUsageType.Mentions)
1204 post.IsExcludeReply = excluded;
1206 // 除外ルール適用のないReplyならReplyタブに追加
1207 if (post.IsReply && !excluded)
1208 tab.AddPostImmediately(post.StatusId, post.IsRead);
1212 // フィルタの更新によってタブから取り除かれたツイートのID
1213 var detachedIds = orgIds.Except(tab.BackupIds).ToArray();
1215 detachedIdsAll = detachedIdsAll.Concat(detachedIds);
1218 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
1219 foreach (var id in detachedIdsAll)
1222 foreach (var tbTemp in _tabs.Values.ToArray())
1224 if (!tbTemp.IsDistributableTabType)
1227 if (tbTemp.Contains(id))
1237 if (this._statuses.TryGetValue(id, out post))
1238 homeTab.AddPostImmediately(post.StatusId, post.IsRead);
1244 public void ClearTabIds(string TabName)
1249 var tb = _tabs[TabName];
1250 if (!tb.IsInnerStorageTabType)
1252 foreach (var Id in tb.BackupIds)
1255 foreach (var tab in _tabs.Values)
1257 if (tab.Contains(Id))
1265 PostClass removedPost;
1266 _statuses.TryRemove(Id, out removedPost);
1276 public void RefreshOwl(ISet<long> follower)
1280 if (follower.Count > 0)
1282 foreach (var post in _statuses.Values)
1284 //if (post.UserId = 0 || post.IsDm) Continue For
1291 post.IsOwl = !follower.Contains(post.UserId);
1297 foreach (var post in _statuses.Values)
1305 public TabClass GetTabByType(MyCommon.TabUsageType tabType)
1307 //Home,Mentions,DM,Favは1つに制限する
1308 //その他のタイプを指定されたら、最初に合致したものを返す
1312 return this._tabs.Values
1313 .FirstOrDefault(x => x.TabType.HasFlag(tabType));
1317 public TabClass[] GetTabsByType(MyCommon.TabUsageType tabType)
1321 return this._tabs.Values
1322 .Where(x => x.TabType.HasFlag(tabType))
1327 public TabClass[] GetTabsInnerStorageType()
1331 return this._tabs.Values
1332 .Where(x => x.IsInnerStorageTabType)
1337 public TabClass GetTabByName(string tabName)
1342 return _tabs.TryGetValue(tabName, out tab)
1348 public ConcurrentDictionary<long, PostClass> Posts
1353 public sealed class TabClass
1355 private List<PostFilterRule> _filters;
1356 private IndexedSortedSet<long> _ids;
1357 private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
1358 private SortedSet<long> unreadIds = new SortedSet<long>();
1359 private MyCommon.TabUsageType _tabType = MyCommon.TabUsageType.Undefined;
1361 private readonly object _lockObj = new object();
1363 public string User { get; set; }
1367 private string _searchLang = "";
1368 private string _searchWords = "";
1370 public string SearchLang
1378 _searchLang = value;
1379 this.ResetFetchIds();
1382 public string SearchWords
1386 return _searchWords;
1390 _searchWords = value.Trim();
1391 this.ResetFetchIds();
1395 private Dictionary<string, string> _beforeQuery = new Dictionary<string, string>();
1397 public bool IsSearchQueryChanged
1401 var qry = new Dictionary<string, string>();
1402 if (!string.IsNullOrEmpty(_searchWords))
1404 qry.Add("q", _searchWords);
1405 if (!string.IsNullOrEmpty(_searchLang)) qry.Add("lang", _searchLang);
1407 if (qry.Count != _beforeQuery.Count)
1413 foreach (var kvp in qry)
1416 if (!_beforeQuery.TryGetValue(kvp.Key, out value) || value != kvp.Value)
1429 private ListElement _listInfo;
1430 public ListElement ListInfo
1444 public PostClass RelationTargetPost { get; set; }
1447 public long OldestId = long.MaxValue;
1450 public long SinceId { get; set; }
1453 public ConcurrentDictionary<long, PostClass> Posts { get; private set; }
1455 private ConcurrentDictionary<long, PostClass> _innerPosts;
1457 private struct TemporaryId
1462 public TemporaryId(long argId, bool argRead)
1471 _innerPosts = new ConcurrentDictionary<long, PostClass>();
1472 Posts = _innerPosts;
1475 _filters = new List<PostFilterRule>();
1479 UnreadManage = true;
1480 _ids = new IndexedSortedSet<long>();
1481 _tabType = MyCommon.TabUsageType.Undefined;
1485 public TabClass(string TabName, MyCommon.TabUsageType TabType, ListElement list) : this()
1487 this.TabName = TabName;
1488 this.TabType = TabType;
1489 this.ListInfo = list;
1493 /// タブ更新時に使用する SinceId, OldestId をリセットする
1495 public void ResetFetchIds()
1498 this.OldestId = long.MaxValue;
1502 /// ソート対象のフィールドとソート順を設定し、ソートを実行します
1504 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
1506 this.SortMode = mode;
1507 this.SortOrder = sortOrder;
1509 this.ApplySortMode();
1512 private void ApplySortMode()
1514 var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
1516 Comparison<long> comparison;
1517 if (this.SortMode == ComparerMode.Id)
1519 comparison = (x, y) => sign * x.CompareTo(y);
1523 Comparison<PostClass> postComparison;
1524 switch (this.SortMode)
1526 case ComparerMode.Data:
1527 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
1529 case ComparerMode.Name:
1530 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
1532 case ComparerMode.Nickname:
1533 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
1535 case ComparerMode.Source:
1536 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
1539 throw new InvalidEnumArgumentException();
1542 comparison = (x, y) =>
1544 PostClass xPost, yPost;
1545 this.Posts.TryGetValue(x, out xPost);
1546 this.Posts.TryGetValue(y, out yPost);
1548 var compare = sign * postComparison(xPost, yPost);
1552 // 同値であれば status_id で比較する
1553 return sign * x.CompareTo(y);
1557 var comparer = Comparer<long>.Create(comparison);
1559 this._ids = new IndexedSortedSet<long>(this._ids, comparer);
1560 this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
1564 public ComparerMode SortMode { get; private set; }
1567 public SortOrder SortOrder { get; private set; }
1569 public void AddPostQueue(long statusId, bool read)
1571 this.addQueue.Enqueue(new TemporaryId(statusId, read));
1575 public void AddPostImmediately(long ID, bool Read)
1577 if (this._ids.Contains(ID)) return;
1582 this.unreadIds.Add(ID);
1586 public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false)
1588 if (this.IsInnerStorageTabType) return MyCommon.HITRESULT.None;
1590 var rslt = MyCommon.HITRESULT.None;
1592 lock (this._lockObj)
1594 foreach (var ft in _filters)
1598 switch (ft.ExecFilter(post)) //フィルタクラスでヒット判定
1600 case MyCommon.HITRESULT.None:
1602 case MyCommon.HITRESULT.Copy:
1603 if (rslt != MyCommon.HITRESULT.CopyAndMark) rslt = MyCommon.HITRESULT.Copy;
1605 case MyCommon.HITRESULT.CopyAndMark:
1606 rslt = MyCommon.HITRESULT.CopyAndMark;
1608 case MyCommon.HITRESULT.Move:
1609 rslt = MyCommon.HITRESULT.Move;
1611 case MyCommon.HITRESULT.Exclude:
1612 rslt = MyCommon.HITRESULT.Exclude;
1616 catch (NullReferenceException)
1618 // ExecFilterでNullRef出る場合あり。暫定対応
1619 MyCommon.TraceOut("ExecFilterでNullRef: " + ft.ToString());
1620 rslt = MyCommon.HITRESULT.None;
1627 if (this.TabType != MyCommon.TabUsageType.Mute &&
1628 rslt != MyCommon.HITRESULT.None && rslt != MyCommon.HITRESULT.Exclude)
1631 this.AddPostImmediately(post.StatusId, post.IsRead);
1633 this.AddPostQueue(post.StatusId, post.IsRead);
1636 return rslt; //マーク付けは呼び出し元で行うこと
1640 public void AddPostToInnerStorage(PostClass Post)
1642 if (_innerPosts.ContainsKey(Post.StatusId)) return;
1643 _innerPosts.TryAdd(Post.StatusId, Post);
1644 this.AddPostQueue(Post.StatusId, Post.IsRead);
1647 public IList<long> AddSubmit()
1649 var addedIds = new List<long>();
1652 while (this.addQueue.TryDequeue(out tId))
1654 this.AddPostImmediately(tId.Id, tId.Read);
1655 addedIds.Add(tId.Id);
1661 public void Remove(long Id)
1663 if (!this._ids.Contains(Id))
1666 this._ids.Remove(Id);
1667 this.unreadIds.Remove(Id);
1669 if (this.IsInnerStorageTabType)
1671 PostClass removedPost;
1672 this._innerPosts.TryRemove(Id, out removedPost);
1676 public bool UnreadManage { get; set; }
1678 // v1.0.5で「タブを固定(Locked)」から「タブを保護(Protected)」に名称変更
1679 [XmlElement(ElementName = "Locked")]
1680 public bool Protected { get; set; }
1682 public bool Notify { get; set; }
1684 public string SoundFile { get; set; }
1687 /// 次に表示する未読ツイートのIDを返します。
1688 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1691 public long NextUnreadId
1695 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1698 if (this.unreadIds.Count == 0)
1701 // unreadIds はリストのインデックス番号順に並んでいるため、
1702 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
1703 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
1708 /// 次に表示する未読ツイートのインデックス番号を返します。
1709 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1712 public int NextUnreadIndex
1716 var unreadId = this.NextUnreadId;
1717 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
1723 /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
1726 public int UnreadCount
1730 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1733 return this.unreadIds.Count;
1741 return this._ids.Count;
1746 /// 未読ツイートの ID を配列で返します
1748 public long[] GetUnreadIds()
1750 lock (this._lockObj)
1752 return this.unreadIds.ToArray();
1760 /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
1762 /// <param name="statusId">変更するツイートのID</param>
1763 /// <param name="read">既読状態</param>
1764 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1765 internal bool SetReadState(long statusId, bool read)
1767 if (!this._ids.Contains(statusId))
1768 throw new ArgumentException(nameof(statusId));
1770 if (this.IsInnerStorageTabType)
1771 this.Posts[statusId].IsRead = read;
1774 return this.unreadIds.Remove(statusId);
1776 return this.unreadIds.Add(statusId);
1779 public PostFilterRule[] GetFilters()
1781 lock (this._lockObj)
1783 return _filters.ToArray();
1787 public void RemoveFilter(PostFilterRule filter)
1789 lock (this._lockObj)
1791 _filters.Remove(filter);
1792 filter.PropertyChanged -= this.OnFilterModified;
1793 this.FilterModified = true;
1797 public bool AddFilter(PostFilterRule filter)
1799 lock (this._lockObj)
1801 if (_filters.Contains(filter)) return false;
1802 filter.PropertyChanged += this.OnFilterModified;
1803 _filters.Add(filter);
1804 this.FilterModified = true;
1809 private void OnFilterModified(object sender, PropertyChangedEventArgs e)
1811 this.FilterModified = true;
1814 public PostFilterRule[] FilterArray
1818 lock (this._lockObj)
1820 return _filters.ToArray();
1825 lock (this._lockObj)
1827 foreach (var oldFilter in this._filters)
1829 oldFilter.PropertyChanged -= this.OnFilterModified;
1832 this._filters.Clear();
1833 this.FilterModified = true;
1835 foreach (var newFilter in value)
1837 _filters.Add(newFilter);
1838 newFilter.PropertyChanged += this.OnFilterModified;
1843 public bool Contains(long ID)
1845 return _ids.Contains(ID);
1848 public void ClearIDs()
1851 this.unreadIds.Clear();
1852 _innerPosts.Clear();
1854 Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
1857 public PostClass this[int Index]
1861 var id = GetId(Index);
1862 if (id < 0) throw new ArgumentException("Index can't find. Index=" + Index.ToString() + "/TabName=" + TabName, nameof(Index));
1867 public PostClass[] this[int StartIndex, int EndIndex]
1871 var length = EndIndex - StartIndex + 1;
1872 var posts = new PostClass[length];
1873 for (int i = 0; i < length; i++)
1875 posts[i] = Posts[GetId(StartIndex + i)];
1881 public long[] GetId(ListView.SelectedIndexCollection IndexCollection)
1883 if (IndexCollection.Count == 0) return null;
1885 var Ids = new long[IndexCollection.Count];
1886 for (int i = 0; i < Ids.Length; i++)
1888 Ids[i] = GetId(IndexCollection[i]);
1893 public long GetId(int Index)
1895 return Index < _ids.Count ? _ids[Index] : -1;
1898 public int[] IndexOf(long[] Ids)
1900 if (Ids == null) return null;
1901 var idx = new int[Ids.Length];
1902 for (int i = 0; i < Ids.Length; i++)
1904 idx[i] = IndexOf(Ids[i]);
1909 public int IndexOf(long ID)
1911 return _ids.IndexOf(ID);
1915 public bool FilterModified { get; set; }
1917 public long[] BackupIds
1921 return _ids.ToArray();
1925 public string TabName { get; set; }
1927 public MyCommon.TabUsageType TabType
1936 if (this.IsInnerStorageTabType)
1938 Posts = _innerPosts;
1942 Posts = TabInformations.GetInstance().Posts;
1947 public bool IsDefaultTabType
1951 return _tabType.IsDefault();
1955 public bool IsDistributableTabType
1959 return _tabType.IsDistributable();
1963 public bool IsInnerStorageTabType
1967 return _tabType.IsInnerStorage();
1973 /// enum TabUsageType に対応する拡張メソッドを定義したクラス
1975 public static class TabUsageTypeExt
1977 const MyCommon.TabUsageType DefaultTabTypeMask =
1978 MyCommon.TabUsageType.Home |
1979 MyCommon.TabUsageType.Mentions |
1980 MyCommon.TabUsageType.DirectMessage |
1981 MyCommon.TabUsageType.Favorites |
1982 MyCommon.TabUsageType.Mute;
1984 const MyCommon.TabUsageType DistributableTabTypeMask =
1985 MyCommon.TabUsageType.Mentions |
1986 MyCommon.TabUsageType.UserDefined |
1987 MyCommon.TabUsageType.Mute;
1989 const MyCommon.TabUsageType InnerStorageTabTypeMask =
1990 MyCommon.TabUsageType.DirectMessage |
1991 MyCommon.TabUsageType.PublicSearch |
1992 MyCommon.TabUsageType.Lists |
1993 MyCommon.TabUsageType.UserTimeline |
1994 MyCommon.TabUsageType.Related |
1995 MyCommon.TabUsageType.SearchResults;
1998 /// デフォルトタブかどうかを示す値を取得します。
2000 public static bool IsDefault(this MyCommon.TabUsageType tabType)
2002 return (tabType & DefaultTabTypeMask) != 0;
2006 /// 振り分け可能タブかどうかを示す値を取得します。
2008 public static bool IsDistributable(this MyCommon.TabUsageType tabType)
2010 return (tabType & DistributableTabTypeMask) != 0;
2014 /// 内部ストレージを使用するタブかどうかを示す値を取得します。
2016 public static bool IsInnerStorage(this MyCommon.TabUsageType tabType)
2018 return (tabType & InnerStorageTabTypeMask) != 0;
2025 public enum ComparerMode