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.Diagnostics;
34 using System.Threading.Tasks;
35 using System.Windows.Forms;
36 using OpenTween.Setting;
38 namespace OpenTween.Models
40 public sealed class TabInformations
42 //個別タブの情報をDictionaryで保持
43 public Dictionary<string, TabModel> Tabs { get; } = new Dictionary<string, TabModel>();
44 public ConcurrentDictionary<long, PostClass> Posts { get; } = new ConcurrentDictionary<long, PostClass>();
46 private Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
47 private ConcurrentDictionary<long, int> retweetsCount = new ConcurrentDictionary<long, int>();
49 public Stack<TabModel> RemovedTab { get; } = new Stack<TabModel>();
51 public ISet<long> BlockIds { get; set; } = new HashSet<long>();
52 public ISet<long> MuteUserIds { get; set; } = new HashSet<long>();
55 //AddPost(複数回) -> DistributePosts -> SubmitUpdate
57 private ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
59 /// <summary>通知サウンドを再生する優先順位</summary>
60 private Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
62 [MyCommon.TabUsageType.DirectMessage] = 100,
63 [MyCommon.TabUsageType.Mentions] = 90,
64 [MyCommon.TabUsageType.UserDefined] = 80,
65 [MyCommon.TabUsageType.Home] = 70,
66 [MyCommon.TabUsageType.Favorites] = 60,
70 private readonly object LockObj = new object();
72 private static TabInformations _instance = new TabInformations();
75 private List<ListElement> _lists = new List<ListElement>();
77 private TabInformations()
81 public static TabInformations GetInstance()
82 => _instance; // singleton
84 public string SelectedTabName { get; private set; } = "";
86 public TabModel SelectedTab
87 => this.Tabs[this.SelectedTabName];
89 public List<ListElement> SubscribableLists
94 if (value != null && value.Count > 0)
96 foreach (var tb in this.GetTabsByType<ListTimelineTabModel>())
98 foreach (var list in value)
100 if (tb.ListInfo.Id == list.Id)
112 public bool AddTab(TabModel tab)
116 if (this.Tabs.ContainsKey(tab.TabName))
119 this.Tabs.Add(tab.TabName, tab);
120 tab.SetSortMode(this.SortMode, this.SortOrder);
126 //public void AddTab(string TabName, TabClass Tab)
128 // _tabs.Add(TabName, Tab);
131 public void RemoveTab(string TabName)
135 var tb = GetTabByName(TabName);
136 if (tb.IsDefaultTabType) return; //念のため
138 if (!tb.IsInnerStorageTabType)
140 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
141 var dmTab = GetTabByType(MyCommon.TabUsageType.DirectMessage);
143 for (int idx = 0; idx < tb.AllCount; ++idx)
146 var Id = tb.GetStatusIdAt(idx);
147 if (Id < 0) continue;
148 foreach (var tab in this.Tabs.Values)
150 if (tab != tb && tab != dmTab)
152 if (tab.Contains(Id))
159 if (!exist) homeTab.AddPostImmediately(Id, this.Posts[Id].IsRead);
162 this.RemovedTab.Push(tb);
163 this.Tabs.Remove(TabName);
167 public bool ContainsTab(string TabText)
168 => this.Tabs.ContainsKey(TabText);
170 public bool ContainsTab(TabModel ts)
171 => this.Tabs.ContainsValue(ts);
173 public void SelectTab(string tabName)
175 if (!this.Tabs.ContainsKey(tabName))
176 throw new ArgumentException($"{tabName} does not exist.", nameof(tabName));
178 this.SelectedTabName = tabName;
182 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
184 /// <param name="baseTabName">作成したいタブ名</param>
185 /// <returns>生成されたタブ名</returns>
186 /// <exception cref="TabException">タブ名の生成を 100 回試行して失敗した場合</exception>
187 public string MakeTabName(string baseTabName)
188 => this.MakeTabName(baseTabName, 100);
191 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
193 /// <param name="baseTabName">作成したいタブ名</param>
194 /// <param name="retryCount">重複を避けたタブ名を生成する試行回数</param>
195 /// <returns>生成されたタブ名</returns>
196 /// <exception cref="TabException">retryCount で指定された回数だけタブ名の生成を試行して失敗した場合</exception>
197 public string MakeTabName(string baseTabName, int retryCount)
199 if (!this.ContainsTab(baseTabName))
202 foreach (var i in Enumerable.Range(2, retryCount - 1))
204 var tabName = baseTabName + i;
205 if (!this.ContainsTab(tabName))
211 var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
212 throw new TabException(message);
215 public Dictionary<string, TabModel>.KeyCollection KeysTab
218 public SortOrder SortOrder { get; private set; }
220 public ComparerMode SortMode { get; private set; }
222 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
224 this.SortMode = mode;
225 this.SortOrder = sortOrder;
227 foreach (var tab in this.Tabs.Values)
228 tab.SetSortMode(mode, sortOrder);
231 public SortOrder ToggleSortOrder(ComparerMode sortMode)
233 var sortOrder = this.SortOrder;
235 if (this.SortMode == sortMode)
237 if (sortOrder == SortOrder.Ascending)
238 sortOrder = SortOrder.Descending;
240 sortOrder = SortOrder.Ascending;
244 sortOrder = SortOrder.Ascending;
247 this.SetSortMode(sortMode, sortOrder);
249 return this.SortOrder;
252 // public PostClass RetweetSource(long Id)
256 // if (_retweets.ContainsKey(Id))
258 // return _retweets[Id];
266 public PostClass RetweetSource(long Id)
267 => this.Posts.TryGetValue(Id, out var status) ? status : null;
269 public void ScrubGeoReserve(long id, long upToStatusId)
273 //this._scrubGeo.Add(new ScrubGeoInfo With {.UserId = id, .UpToStatusId = upToStatusId});
274 this.ScrubGeo(id, upToStatusId);
278 private void ScrubGeo(long userId, long upToStatusId)
282 var userPosts = from post in this.Posts.Values
283 where post.UserId == userId && post.UserId <= upToStatusId
286 foreach (var p in userPosts)
291 var userPosts2 = from tb in this.GetTabsInnerStorageType()
292 from post in tb.Posts.Values
293 where post.UserId == userId && post.UserId <= upToStatusId
296 foreach (var p in userPosts2)
303 public void RemovePostFromAllTabs(long statusId, bool setIsDeleted)
305 foreach (var tab in this.Tabs.Values)
307 tab.EnqueueRemovePost(statusId, setIsDeleted);
312 if (this.Posts.TryGetValue(statusId, out var post))
313 post.IsDeleted = true;
317 public int SubmitUpdate()
318 => this.SubmitUpdate(out var soundFile, out var notifyPosts, out var newMentionOrDm, out var isDeletePost);
320 public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
321 out bool newMentionOrDm, out bool isDeletePost)
327 notifyPosts = Array.Empty<PostClass>();
328 newMentionOrDm = false;
329 isDeletePost = false;
331 var addedCountTotal = 0;
332 var removedIdsAll = new List<long>();
333 var notifyPostsList = new List<PostClass>();
335 var currentNotifyPriority = -1;
337 foreach (var tab in this.Tabs.Values)
340 var addedIds = tab.AddSubmit();
342 if (tab.TabType == MyCommon.TabUsageType.Mentions ||
343 tab.TabType == MyCommon.TabUsageType.DirectMessage)
345 if (addedIds.Count > 0)
346 newMentionOrDm = true;
349 if (addedIds.Count != 0)
354 foreach (var statusId in addedIds)
356 if (tab.Posts.TryGetValue(statusId, out var post))
357 notifyPostsList.Add(post);
361 // 通知サウンドは TabClass.Notify の値に関わらず鳴らす
362 // SettingCommon.PlaySound が false であれば TweenMain 側で無効化される
363 if (!string.IsNullOrEmpty(tab.SoundFile))
365 if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out var notifyPriority))
368 if (notifyPriority > currentNotifyPriority)
371 soundFile = tab.SoundFile;
372 currentNotifyPriority = notifyPriority;
377 addedCountTotal += addedIds.Count;
379 var removedIds = tab.RemoveSubmit();
380 removedIdsAll.AddRange(removedIds);
383 notifyPosts = notifyPostsList.Distinct().ToArray();
385 if (removedIdsAll.Count > 0)
388 foreach (var removedId in removedIdsAll.Distinct())
391 foreach (var tab in this.Tabs.Values)
393 if (tab.Contains(removedId))
400 // 全てのタブから表示されなくなった発言は this._statuses からも削除する
402 this.Posts.TryRemove(removedId, out var removedPost);
405 return addedCountTotal;
409 public int DistributePosts()
413 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
414 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
415 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
417 var distributableTabs = this.GetTabsByType<FilterTabModel>()
422 while (this.addQueue.TryDequeue(out var statusId))
424 if (!this.Posts.TryGetValue(statusId, out var post))
427 var filterHit = false; // フィルタにヒットしたタブがあるか
428 var mark = false; // フィルタによってマーク付けされたか
429 var excludedReply = false; // リプライから除外されたか
430 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
432 foreach (var tab in distributableTabs)
435 switch (tab.AddFiltered(post))
437 case MyCommon.HITRESULT.Copy:
440 case MyCommon.HITRESULT.CopyAndMark:
444 case MyCommon.HITRESULT.Move:
448 case MyCommon.HITRESULT.None:
450 case MyCommon.HITRESULT.Exclude:
451 if (tab.TabType == MyCommon.TabUsageType.Mentions)
452 excludedReply = true;
457 post.FilterHit = filterHit;
459 post.IsExcludeReply = excludedReply;
461 // 移動されなかったらRecentに追加
463 homeTab.AddPostQueue(post);
465 // 除外ルール適用のないReplyならReplyタブに追加
466 if (post.IsReply && !excludedReply)
467 replyTab.AddPostQueue(post);
469 // Fav済み発言だったらFavoritesタブに追加
471 favTab.AddPostQueue(post);
480 public void AddPost(PostClass Item)
482 Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
486 if (this.IsMuted(Item, isHomeTimeline: true))
489 if (Posts.TryGetValue(Item.StatusId, out var status))
493 if (Item.RetweetedId == null)
504 return; //追加済みなら何もしない
509 if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
512 if (Item.RetweetedId != null && SettingManager.Common.HideDuplicatedRetweets)
514 var retweetCount = this.UpdateRetweetCount(Item);
516 if (retweetCount > 1 && !Item.IsMe)
520 if (BlockIds.Contains(Item.UserId))
523 Posts.TryAdd(Item.StatusId, Item);
525 if (Item.IsFav && this.retweetsCount.ContainsKey(Item.StatusId))
527 return; //Fav済みのRetweet元発言は追加しない
529 this.addQueue.Enqueue(Item.StatusId);
533 public bool IsMuted(PostClass post, bool isHomeTimeline)
535 var muteTab = this.GetTabByType<MuteTabModel>();
536 if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
539 // これ以降は Twitter 標準のミュート機能に準じた判定
540 // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
542 // ホームタイムライン以外 (検索・リストなど) は対象外
550 if (this.MuteUserIds.Contains(post.UserId))
553 if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
559 private int UpdateRetweetCount(PostClass retweetPost)
561 var retweetedId = retweetPost.RetweetedId.Value;
563 return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1);
566 public bool AddQuoteTweet(PostClass item)
570 if (IsMuted(item, isHomeTimeline: false) || BlockIds.Contains(item.UserId))
573 _quotes[item.StatusId] = item;
579 /// 全てのタブを横断して既読状態を変更します
581 /// <param name="statusId">変更するツイートのID</param>
582 /// <param name="read">既読状態</param>
583 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
584 public bool SetReadAllTab(long statusId, bool read)
588 foreach (var tab in this.Tabs.Values)
590 if (!tab.Contains(statusId))
593 tab.SetReadState(statusId, read);
596 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
597 if (this.Posts.TryGetValue(statusId, out var post))
605 /// Home タブのツイートを全て既読にします。
606 /// ただし IsReply または FilterHit が true なものを除きます。
608 public void SetReadHomeTab()
610 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
614 foreach (var statusId in homeTab.GetUnreadIds())
616 if (!this.Posts.TryGetValue(statusId, out var post))
619 if (post.IsReply || post.FilterHit)
622 this.SetReadAllTab(post.StatusId, read: true);
627 public PostClass this[long ID]
631 if (this.Posts.TryGetValue(ID, out var status))
634 if (this._quotes.TryGetValue(ID, out status))
637 return this.GetTabsInnerStorageType()
638 .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
639 .FirstOrDefault(x => x != null);
643 public bool ContainsKey(long Id)
648 return Posts.ContainsKey(Id);
652 public void RenameTab(string Original, string NewName)
656 var tb = this.Tabs[Original];
657 this.Tabs.Remove(Original);
658 tb.TabName = NewName;
659 this.Tabs.Add(NewName, tb);
663 public void FilterAll()
667 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
668 var detachedIdsAll = Enumerable.Empty<long>();
670 foreach (var tab in this.Tabs.Values.OfType<FilterTabModel>().ToArray())
672 if (tab.TabType == MyCommon.TabUsageType.Mute)
675 // フィルタに変更のあったタブのみを対象とする
676 if (!tab.FilterModified)
679 tab.FilterModified = false;
681 // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
682 var orgIds = tab.StatusIds;
685 foreach (var post in Posts.Values)
687 var filterHit = false; // フィルタにヒットしたタブがあるか
688 var mark = false; // フィルタによってマーク付けされたか
689 var excluded = false; // 除外フィルタによって除外されたか
690 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
692 switch (tab.AddFiltered(post, immediately: true))
694 case MyCommon.HITRESULT.Copy:
697 case MyCommon.HITRESULT.CopyAndMark:
701 case MyCommon.HITRESULT.Move:
705 case MyCommon.HITRESULT.None:
707 case MyCommon.HITRESULT.Exclude:
712 post.FilterHit = filterHit;
717 homeTab.RemovePostImmediately(post.StatusId);
719 if (tab.TabType == MyCommon.TabUsageType.Mentions)
721 post.IsExcludeReply = excluded;
723 // 除外ルール適用のないReplyならReplyタブに追加
724 if (post.IsReply && !excluded)
725 tab.AddPostImmediately(post.StatusId, post.IsRead);
729 // フィルタの更新によってタブから取り除かれたツイートのID
730 var detachedIds = orgIds.Except(tab.StatusIds).ToArray();
732 detachedIdsAll = detachedIdsAll.Concat(detachedIds);
735 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
736 foreach (var id in detachedIdsAll)
739 foreach (var tbTemp in this.Tabs.Values.ToArray())
741 if (!tbTemp.IsDistributableTabType)
744 if (tbTemp.Contains(id))
753 if (this.Posts.TryGetValue(id, out var post))
754 homeTab.AddPostImmediately(post.StatusId, post.IsRead);
760 public void ClearTabIds(string TabName)
765 var tb = this.Tabs[TabName];
766 if (!tb.IsInnerStorageTabType)
768 foreach (var Id in tb.StatusIds)
771 foreach (var tab in this.Tabs.Values)
773 if (tab.Contains(Id))
780 Posts.TryRemove(Id, out var removedPost);
789 public void RefreshOwl(ISet<long> follower)
793 if (follower.Count > 0)
795 foreach (var post in Posts.Values)
797 //if (post.UserId = 0 || post.IsDm) Continue For
804 post.IsOwl = !follower.Contains(post.UserId);
810 foreach (var post in Posts.Values)
818 public TabModel GetTabByType(MyCommon.TabUsageType tabType)
820 //Home,Mentions,DM,Favは1つに制限する
821 //その他のタイプを指定されたら、最初に合致したものを返す
825 return this.Tabs.Values
826 .FirstOrDefault(x => x.TabType.HasFlag(tabType));
830 public T GetTabByType<T>() where T : TabModel
833 return this.Tabs.Values.OfType<T>().FirstOrDefault();
836 public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType)
840 return this.Tabs.Values
841 .Where(x => x.TabType.HasFlag(tabType))
846 public T[] GetTabsByType<T>() where T : TabModel
849 return this.Tabs.Values.OfType<T>().ToArray();
852 public TabModel[] GetTabsInnerStorageType()
856 return this.Tabs.Values
857 .Where(x => x.IsInnerStorageTabType)
862 public TabModel GetTabByName(string tabName)
866 return this.Tabs.TryGetValue(tabName, out var tab)