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 IReadOnlyTabCollection Tabs
46 public MuteTabModel MuteTab { get; private set; }
48 public ConcurrentDictionary<long, PostClass> Posts { get; } = new ConcurrentDictionary<long, PostClass>();
50 private readonly Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
51 private readonly ConcurrentDictionary<long, int> retweetsCount = new ConcurrentDictionary<long, int>();
53 public Stack<TabModel> RemovedTab { get; } = new Stack<TabModel>();
55 public ISet<long> BlockIds { get; set; } = new HashSet<long>();
56 public ISet<long> MuteUserIds { get; set; } = new HashSet<long>();
59 //AddPost(複数回) -> DistributePosts -> SubmitUpdate
61 private readonly TabCollection tabs = new TabCollection();
62 private readonly ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
64 /// <summary>通知サウンドを再生する優先順位</summary>
65 private readonly Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
67 [MyCommon.TabUsageType.DirectMessage] = 100,
68 [MyCommon.TabUsageType.Mentions] = 90,
69 [MyCommon.TabUsageType.UserDefined] = 80,
70 [MyCommon.TabUsageType.Home] = 70,
71 [MyCommon.TabUsageType.Favorites] = 60,
75 private readonly object LockObj = new object();
77 private static readonly TabInformations _instance = new TabInformations();
80 private List<ListElement> _lists = new List<ListElement>();
82 private TabInformations()
86 public static TabInformations GetInstance()
87 => _instance; // singleton
89 public string SelectedTabName { get; private set; } = "";
91 public TabModel SelectedTab
92 => this.Tabs[this.SelectedTabName];
94 public int SelectedTabIndex
95 => this.Tabs.IndexOf(this.SelectedTabName);
97 public List<ListElement> SubscribableLists
102 if (value != null && value.Count > 0)
104 foreach (var tb in this.GetTabsByType<ListTimelineTabModel>())
106 foreach (var list in value)
108 if (tb.ListInfo.Id == list.Id)
120 public bool AddTab(TabModel tab)
124 if (tab is MuteTabModel muteTab)
126 if (this.MuteTab != null)
129 this.MuteTab = muteTab;
133 if (this.Tabs.Contains(tab.TabName))
137 tab.SetSortMode(this.SortMode, this.SortOrder);
143 //public void AddTab(string TabName, TabClass Tab)
145 // _tabs.Add(TabName, Tab);
148 public void RemoveTab(string TabName)
152 var tb = GetTabByName(TabName);
153 if (tb.IsDefaultTabType) return; //念のため
155 if (!tb.IsInnerStorageTabType)
157 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
158 var dmTab = GetTabByType(MyCommon.TabUsageType.DirectMessage);
160 for (var idx = 0; idx < tb.AllCount; ++idx)
163 var Id = tb.GetStatusIdAt(idx);
164 if (Id < 0) continue;
165 foreach (var tab in this.Tabs)
167 if (tab != tb && tab != dmTab)
169 if (tab.Contains(Id))
176 if (!exist) homeTab.AddPostImmediately(Id, this.Posts[Id].IsRead);
179 this.RemovedTab.Push(tb);
180 this.tabs.Remove(TabName);
184 public void ReplaceTab(TabModel tab)
186 if (!this.ContainsTab(tab.TabName))
187 throw new ArgumentOutOfRangeException(nameof(tab));
189 var index = this.tabs.IndexOf(tab);
190 this.tabs.RemoveAt(index);
191 this.tabs.Insert(index, tab);
194 public void MoveTab(int newIndex, TabModel tab)
196 if (!this.ContainsTab(tab))
197 throw new ArgumentOutOfRangeException(nameof(tab));
199 this.tabs.Remove(tab);
200 this.tabs.Insert(newIndex, tab);
203 public bool ContainsTab(string TabText)
204 => this.Tabs.Contains(TabText);
206 public bool ContainsTab(TabModel ts)
207 => this.Tabs.Contains(ts);
209 public void SelectTab(string tabName)
211 if (!this.Tabs.Contains(tabName))
212 throw new ArgumentException($"{tabName} does not exist.", nameof(tabName));
214 this.SelectedTabName = tabName;
218 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
220 /// <param name="baseTabName">作成したいタブ名</param>
221 /// <returns>生成されたタブ名</returns>
222 /// <exception cref="TabException">タブ名の生成を 100 回試行して失敗した場合</exception>
223 public string MakeTabName(string baseTabName)
224 => this.MakeTabName(baseTabName, 100);
227 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
229 /// <param name="baseTabName">作成したいタブ名</param>
230 /// <param name="retryCount">重複を避けたタブ名を生成する試行回数</param>
231 /// <returns>生成されたタブ名</returns>
232 /// <exception cref="TabException">retryCount で指定された回数だけタブ名の生成を試行して失敗した場合</exception>
233 public string MakeTabName(string baseTabName, int retryCount)
235 if (!this.ContainsTab(baseTabName))
238 foreach (var i in Enumerable.Range(2, retryCount - 1))
240 var tabName = baseTabName + i;
241 if (!this.ContainsTab(tabName))
247 var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
248 throw new TabException(message);
251 public SortOrder SortOrder { get; private set; }
253 public ComparerMode SortMode { get; private set; }
255 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
257 this.SortMode = mode;
258 this.SortOrder = sortOrder;
260 foreach (var tab in this.Tabs)
261 tab.SetSortMode(mode, sortOrder);
264 public SortOrder ToggleSortOrder(ComparerMode sortMode)
266 var sortOrder = this.SortOrder;
268 if (this.SortMode == sortMode)
270 if (sortOrder == SortOrder.Ascending)
271 sortOrder = SortOrder.Descending;
273 sortOrder = SortOrder.Ascending;
277 sortOrder = SortOrder.Ascending;
280 this.SetSortMode(sortMode, sortOrder);
282 return this.SortOrder;
285 // public PostClass RetweetSource(long Id)
289 // if (_retweets.ContainsKey(Id))
291 // return _retweets[Id];
299 public PostClass RetweetSource(long Id)
300 => this.Posts.TryGetValue(Id, out var status) ? status : null;
302 public void ScrubGeoReserve(long id, long upToStatusId)
306 //this._scrubGeo.Add(new ScrubGeoInfo With {.UserId = id, .UpToStatusId = upToStatusId});
307 this.ScrubGeo(id, upToStatusId);
311 private void ScrubGeo(long userId, long upToStatusId)
315 var userPosts = from post in this.Posts.Values
316 where post.UserId == userId && post.UserId <= upToStatusId
319 foreach (var p in userPosts)
324 var userPosts2 = from tb in this.GetTabsInnerStorageType()
325 from post in tb.Posts.Values
326 where post.UserId == userId && post.UserId <= upToStatusId
329 foreach (var p in userPosts2)
336 public void RemovePostFromAllTabs(long statusId, bool setIsDeleted)
338 foreach (var tab in this.Tabs)
340 tab.EnqueueRemovePost(statusId, setIsDeleted);
345 if (this.Posts.TryGetValue(statusId, out var post))
346 post.IsDeleted = true;
350 public int SubmitUpdate()
351 => this.SubmitUpdate(out var soundFile, out var notifyPosts, out var newMentionOrDm, out var isDeletePost);
353 public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
354 out bool newMentionOrDm, out bool isDeletePost)
360 notifyPosts = Array.Empty<PostClass>();
361 newMentionOrDm = false;
362 isDeletePost = false;
364 var addedCountTotal = 0;
365 var removedIdsAll = new List<long>();
366 var notifyPostsList = new List<PostClass>();
368 var currentNotifyPriority = -1;
370 foreach (var tab in this.Tabs)
373 var addedIds = tab.AddSubmit();
375 if (tab.TabType == MyCommon.TabUsageType.Mentions ||
376 tab.TabType == MyCommon.TabUsageType.DirectMessage)
378 if (addedIds.Count > 0)
379 newMentionOrDm = true;
382 if (addedIds.Count != 0)
387 foreach (var statusId in addedIds)
389 if (tab.Posts.TryGetValue(statusId, out var post))
390 notifyPostsList.Add(post);
394 // 通知サウンドは TabClass.Notify の値に関わらず鳴らす
395 // SettingCommon.PlaySound が false であれば TweenMain 側で無効化される
396 if (!string.IsNullOrEmpty(tab.SoundFile))
398 if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out var notifyPriority))
401 if (notifyPriority > currentNotifyPriority)
404 soundFile = tab.SoundFile;
405 currentNotifyPriority = notifyPriority;
410 addedCountTotal += addedIds.Count;
412 var removedIds = tab.RemoveSubmit();
413 removedIdsAll.AddRange(removedIds);
416 notifyPosts = notifyPostsList.Distinct().ToArray();
418 if (removedIdsAll.Count > 0)
421 foreach (var removedId in removedIdsAll.Distinct())
424 foreach (var tab in this.Tabs)
426 if (tab.Contains(removedId))
433 // 全てのタブから表示されなくなった発言は this._statuses からも削除する
435 this.Posts.TryRemove(removedId, out var removedPost);
438 return addedCountTotal;
442 public int DistributePosts()
446 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
447 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
448 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
450 var distributableTabs = this.GetTabsByType<FilterTabModel>()
455 while (this.addQueue.TryDequeue(out var statusId))
457 if (!this.Posts.TryGetValue(statusId, out var post))
460 var filterHit = false; // フィルタにヒットしたタブがあるか
461 var mark = false; // フィルタによってマーク付けされたか
462 var excludedReply = false; // リプライから除外されたか
463 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
465 foreach (var tab in distributableTabs)
468 switch (tab.AddFiltered(post))
470 case MyCommon.HITRESULT.Copy:
473 case MyCommon.HITRESULT.CopyAndMark:
477 case MyCommon.HITRESULT.Move:
481 case MyCommon.HITRESULT.None:
483 case MyCommon.HITRESULT.Exclude:
484 if (tab.TabType == MyCommon.TabUsageType.Mentions)
485 excludedReply = true;
490 post.FilterHit = filterHit;
492 post.IsExcludeReply = excludedReply;
494 // 移動されなかったらRecentに追加
496 homeTab.AddPostQueue(post);
498 // 除外ルール適用のないReplyならReplyタブに追加
499 if (post.IsReply && !excludedReply)
500 replyTab.AddPostQueue(post);
502 // Fav済み発言だったらFavoritesタブに追加
504 favTab.AddPostQueue(post);
513 public void AddPost(PostClass Item)
515 Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
519 if (this.IsMuted(Item, isHomeTimeline: true))
522 if (Posts.TryGetValue(Item.StatusId, out var status))
526 if (Item.RetweetedId == null)
537 return; //追加済みなら何もしない
542 if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
545 if (Item.RetweetedId != null && SettingManager.Common.HideDuplicatedRetweets)
547 var retweetCount = this.UpdateRetweetCount(Item);
549 if (retweetCount > 1 && !Item.IsMe)
553 if (BlockIds.Contains(Item.UserId))
556 Posts.TryAdd(Item.StatusId, Item);
558 if (Item.IsFav && this.retweetsCount.ContainsKey(Item.StatusId))
560 return; //Fav済みのRetweet元発言は追加しない
562 this.addQueue.Enqueue(Item.StatusId);
566 public bool IsMuted(PostClass post, bool isHomeTimeline)
568 var muteTab = this.MuteTab;
569 if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
572 // これ以降は Twitter 標準のミュート機能に準じた判定
573 // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
575 // ホームタイムライン以外 (検索・リストなど) は対象外
583 if (this.MuteUserIds.Contains(post.UserId))
586 if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
592 private int UpdateRetweetCount(PostClass retweetPost)
594 var retweetedId = retweetPost.RetweetedId.Value;
596 return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1);
599 public bool AddQuoteTweet(PostClass item)
603 if (IsMuted(item, isHomeTimeline: false) || BlockIds.Contains(item.UserId))
606 _quotes[item.StatusId] = item;
612 /// 全てのタブを横断して既読状態を変更します
614 /// <param name="statusId">変更するツイートのID</param>
615 /// <param name="read">既読状態</param>
616 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
617 public bool SetReadAllTab(long statusId, bool read)
621 foreach (var tab in this.Tabs)
623 if (!tab.Contains(statusId))
626 tab.SetReadState(statusId, read);
629 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
630 if (this.Posts.TryGetValue(statusId, out var post))
638 /// Home タブのツイートを全て既読にします。
639 /// ただし IsReply または FilterHit が true なものを除きます。
641 public void SetReadHomeTab()
643 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
647 foreach (var statusId in homeTab.GetUnreadIds())
649 if (!this.Posts.TryGetValue(statusId, out var post))
652 if (post.IsReply || post.FilterHit)
655 this.SetReadAllTab(post.StatusId, read: true);
660 public PostClass this[long ID]
664 if (this.Posts.TryGetValue(ID, out var status))
667 if (this._quotes.TryGetValue(ID, out status))
670 return this.GetTabsInnerStorageType()
671 .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
672 .FirstOrDefault(x => x != null);
676 public bool ContainsKey(long Id)
681 return Posts.ContainsKey(Id);
685 public void RenameTab(string Original, string NewName)
689 var index = this.Tabs.IndexOf(Original);
690 var tb = this.Tabs[Original];
691 this.tabs.RemoveAt(index);
692 tb.TabName = NewName;
693 this.tabs.Insert(index, tb);
697 public void FilterAll()
701 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
702 var detachedIdsAll = Enumerable.Empty<long>();
704 foreach (var tab in this.Tabs.OfType<FilterTabModel>().ToArray())
706 // フィルタに変更のあったタブのみを対象とする
707 if (!tab.FilterModified)
710 tab.FilterModified = false;
712 // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
713 var orgIds = tab.StatusIds;
716 foreach (var post in Posts.Values)
718 var filterHit = false; // フィルタにヒットしたタブがあるか
719 var mark = false; // フィルタによってマーク付けされたか
720 var excluded = false; // 除外フィルタによって除外されたか
721 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
723 switch (tab.AddFiltered(post, immediately: true))
725 case MyCommon.HITRESULT.Copy:
728 case MyCommon.HITRESULT.CopyAndMark:
732 case MyCommon.HITRESULT.Move:
736 case MyCommon.HITRESULT.None:
738 case MyCommon.HITRESULT.Exclude:
743 post.FilterHit = filterHit;
748 homeTab.RemovePostImmediately(post.StatusId);
750 if (tab.TabType == MyCommon.TabUsageType.Mentions)
752 post.IsExcludeReply = excluded;
754 // 除外ルール適用のないReplyならReplyタブに追加
755 if (post.IsReply && !excluded)
756 tab.AddPostImmediately(post.StatusId, post.IsRead);
760 // フィルタの更新によってタブから取り除かれたツイートのID
761 var detachedIds = orgIds.Except(tab.StatusIds).ToArray();
763 detachedIdsAll = detachedIdsAll.Concat(detachedIds);
766 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
767 foreach (var id in detachedIdsAll)
770 foreach (var tbTemp in this.Tabs.ToArray())
772 if (!tbTemp.IsDistributableTabType)
775 if (tbTemp.Contains(id))
784 if (this.Posts.TryGetValue(id, out var post))
785 homeTab.AddPostImmediately(post.StatusId, post.IsRead);
791 public void ClearTabIds(string TabName)
796 var tb = this.Tabs[TabName];
797 if (!tb.IsInnerStorageTabType)
799 foreach (var Id in tb.StatusIds)
802 foreach (var tab in this.Tabs)
804 if (tab.Contains(Id))
811 Posts.TryRemove(Id, out var removedPost);
820 public void RefreshOwl(ISet<long> follower)
824 if (follower.Count > 0)
826 foreach (var post in Posts.Values)
828 //if (post.UserId = 0 || post.IsDm) Continue For
835 post.IsOwl = !follower.Contains(post.UserId);
841 foreach (var post in Posts.Values)
849 public TabModel GetTabByType(MyCommon.TabUsageType tabType)
851 //Home,Mentions,DM,Favは1つに制限する
852 //その他のタイプを指定されたら、最初に合致したものを返す
856 return this.Tabs.FirstOrDefault(x => x.TabType.HasFlag(tabType));
860 public T GetTabByType<T>() where T : TabModel
863 return this.Tabs.OfType<T>().FirstOrDefault();
866 public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType)
871 .Where(x => x.TabType.HasFlag(tabType))
876 public T[] GetTabsByType<T>() where T : TabModel
879 return this.Tabs.OfType<T>().ToArray();
882 public TabModel[] GetTabsInnerStorageType()
887 .Where(x => x.IsInnerStorageTabType)
892 public TabModel GetTabByName(string tabName)
896 return this.Tabs.TryGetValue(tabName, out var tab)