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.
31 using System.Collections.Concurrent;
32 using System.Collections.Generic;
33 using System.Diagnostics;
36 using System.Threading.Tasks;
37 using System.Windows.Forms;
38 using OpenTween.Setting;
40 namespace OpenTween.Models
42 public sealed class TabInformations
44 //個別タブの情報をDictionaryで保持
45 public IReadOnlyTabCollection Tabs
48 public MuteTabModel MuteTab { get; private set; } = new MuteTabModel();
50 public ConcurrentDictionary<long, PostClass> Posts { get; } = new ConcurrentDictionary<long, PostClass>();
52 private readonly Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
53 private readonly ConcurrentDictionary<long, int> retweetsCount = new ConcurrentDictionary<long, int>();
55 public Stack<TabModel> RemovedTab { get; } = new Stack<TabModel>();
57 public ISet<long> BlockIds { get; set; } = new HashSet<long>();
58 public ISet<long> MuteUserIds { get; set; } = new HashSet<long>();
61 //AddPost(複数回) -> DistributePosts -> SubmitUpdate
63 private readonly TabCollection tabs = new TabCollection();
64 private readonly ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
66 /// <summary>通知サウンドを再生する優先順位</summary>
67 private readonly Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
69 [MyCommon.TabUsageType.DirectMessage] = 100,
70 [MyCommon.TabUsageType.Mentions] = 90,
71 [MyCommon.TabUsageType.UserDefined] = 80,
72 [MyCommon.TabUsageType.Home] = 70,
73 [MyCommon.TabUsageType.Favorites] = 60,
77 private readonly object LockObj = new object();
79 private static readonly TabInformations _instance = new TabInformations();
82 private List<ListElement> _lists = new List<ListElement>();
84 private TabInformations()
88 public static TabInformations GetInstance()
89 => _instance; // singleton
91 public string SelectedTabName { get; private set; } = "";
93 public TabModel SelectedTab
94 => this.Tabs[this.SelectedTabName];
96 public int SelectedTabIndex
97 => this.Tabs.IndexOf(this.SelectedTabName);
99 public List<ListElement> SubscribableLists
106 foreach (var tb in this.GetTabsByType<ListTimelineTabModel>())
108 foreach (var list in value)
110 if (tb.ListInfo.Id == list.Id)
122 public bool AddTab(TabModel tab)
126 if (tab is MuteTabModel muteTab)
128 this.MuteTab = muteTab;
132 if (this.Tabs.Contains(tab.TabName))
136 tab.SetSortMode(this.SortMode, this.SortOrder);
142 public void RemoveTab(string TabName)
146 var tb = GetTabByName(TabName);
147 if (tb == null || tb.IsDefaultTabType) return; //念のため
149 if (!tb.IsInnerStorageTabType)
151 var homeTab = this.HomeTab;
152 var dmTab = this.DirectMessageTab;
154 for (var idx = 0; idx < tb.AllCount; ++idx)
157 var Id = tb.GetStatusIdAt(idx);
158 if (Id < 0) continue;
159 foreach (var tab in this.Tabs)
161 if (tab != tb && tab != dmTab)
163 if (tab.Contains(Id))
170 if (!exist) homeTab.AddPostImmediately(Id, this.Posts[Id].IsRead);
173 this.RemovedTab.Push(tb);
174 this.tabs.Remove(TabName);
178 public void ReplaceTab(TabModel tab)
180 if (!this.ContainsTab(tab.TabName))
181 throw new ArgumentOutOfRangeException(nameof(tab));
183 var index = this.tabs.IndexOf(tab);
184 this.tabs.RemoveAt(index);
185 this.tabs.Insert(index, tab);
188 public void MoveTab(int newIndex, TabModel tab)
190 if (!this.ContainsTab(tab))
191 throw new ArgumentOutOfRangeException(nameof(tab));
193 this.tabs.Remove(tab);
194 this.tabs.Insert(newIndex, tab);
197 public bool ContainsTab(string TabText)
198 => this.Tabs.Contains(TabText);
200 public bool ContainsTab(TabModel ts)
201 => this.Tabs.Contains(ts);
203 public void SelectTab(string tabName)
205 if (!this.Tabs.Contains(tabName))
206 throw new ArgumentException($"{tabName} does not exist.", nameof(tabName));
208 this.SelectedTabName = tabName;
212 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
214 /// <param name="baseTabName">作成したいタブ名</param>
215 /// <returns>生成されたタブ名</returns>
216 /// <exception cref="TabException">タブ名の生成を 100 回試行して失敗した場合</exception>
217 public string MakeTabName(string baseTabName)
218 => this.MakeTabName(baseTabName, 100);
221 /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
223 /// <param name="baseTabName">作成したいタブ名</param>
224 /// <param name="retryCount">重複を避けたタブ名を生成する試行回数</param>
225 /// <returns>生成されたタブ名</returns>
226 /// <exception cref="TabException">retryCount で指定された回数だけタブ名の生成を試行して失敗した場合</exception>
227 public string MakeTabName(string baseTabName, int retryCount)
229 if (!this.ContainsTab(baseTabName))
232 foreach (var i in Enumerable.Range(2, retryCount - 1))
234 var tabName = baseTabName + i;
235 if (!this.ContainsTab(tabName))
241 var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
242 throw new TabException(message);
245 public SortOrder SortOrder { get; private set; }
247 public ComparerMode SortMode { get; private set; }
249 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
251 this.SortMode = mode;
252 this.SortOrder = sortOrder;
254 foreach (var tab in this.Tabs)
255 tab.SetSortMode(mode, sortOrder);
258 public SortOrder ToggleSortOrder(ComparerMode sortMode)
260 var sortOrder = this.SortOrder;
262 if (this.SortMode == sortMode)
264 if (sortOrder == SortOrder.Ascending)
265 sortOrder = SortOrder.Descending;
267 sortOrder = SortOrder.Ascending;
271 sortOrder = SortOrder.Ascending;
274 this.SetSortMode(sortMode, sortOrder);
276 return this.SortOrder;
279 public PostClass? RetweetSource(long Id)
280 => this.Posts.TryGetValue(Id, out var status) ? status : null;
282 public void ScrubGeoReserve(long id, long upToStatusId)
285 this.ScrubGeo(id, upToStatusId);
288 private void ScrubGeo(long userId, long upToStatusId)
292 var userPosts = from post in this.Posts.Values
293 where post.UserId == userId && post.UserId <= upToStatusId
296 foreach (var p in userPosts)
301 var userPosts2 = from tb in this.GetTabsInnerStorageType()
302 from post in tb.Posts.Values
303 where post.UserId == userId && post.UserId <= upToStatusId
306 foreach (var p in userPosts2)
313 public void RemovePostFromAllTabs(long statusId, bool setIsDeleted)
315 foreach (var tab in this.Tabs)
317 tab.EnqueueRemovePost(statusId, setIsDeleted);
322 if (this.Posts.TryGetValue(statusId, out var post))
323 post.IsDeleted = true;
327 public int SubmitUpdate()
328 => this.SubmitUpdate(out _, out _, out _, out _);
330 public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
331 out bool newMentionOrDm, out bool isDeletePost)
337 notifyPosts = Array.Empty<PostClass>();
338 newMentionOrDm = false;
339 isDeletePost = false;
341 var addedCountTotal = 0;
342 var removedIdsAll = new List<long>();
343 var notifyPostsList = new List<PostClass>();
345 var currentNotifyPriority = -1;
347 foreach (var tab in this.Tabs)
350 var addedIds = tab.AddSubmit();
352 if (tab.TabType == MyCommon.TabUsageType.Mentions ||
353 tab.TabType == MyCommon.TabUsageType.DirectMessage)
355 if (addedIds.Count > 0)
356 newMentionOrDm = true;
359 if (addedIds.Count != 0)
364 foreach (var statusId in addedIds)
366 if (tab.Posts.TryGetValue(statusId, out var post))
367 notifyPostsList.Add(post);
371 // 通知サウンドは TabClass.Notify の値に関わらず鳴らす
372 // SettingCommon.PlaySound が false であれば TweenMain 側で無効化される
373 if (!string.IsNullOrEmpty(tab.SoundFile))
375 if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out var notifyPriority))
378 if (notifyPriority > currentNotifyPriority)
381 soundFile = tab.SoundFile;
382 currentNotifyPriority = notifyPriority;
387 addedCountTotal += addedIds.Count;
389 var removedIds = tab.RemoveSubmit();
390 removedIdsAll.AddRange(removedIds);
393 notifyPosts = notifyPostsList.Distinct().ToArray();
395 if (removedIdsAll.Count > 0)
398 foreach (var removedId in removedIdsAll.Distinct())
401 foreach (var tab in this.Tabs)
403 if (tab.Contains(removedId))
410 // 全てのタブから表示されなくなった発言は this._statuses からも削除する
412 this.Posts.TryRemove(removedId, out var removedPost);
415 return addedCountTotal;
419 public int DistributePosts()
423 var homeTab = this.HomeTab;
424 var replyTab = this.MentionTab;
425 var favTab = this.FavoriteTab;
427 var distributableTabs = this.GetTabsByType<FilterTabModel>()
432 while (this.addQueue.TryDequeue(out var statusId))
434 if (!this.Posts.TryGetValue(statusId, out var post))
437 var filterHit = false; // フィルタにヒットしたタブがあるか
438 var mark = false; // フィルタによってマーク付けされたか
439 var excludedReply = false; // リプライから除外されたか
440 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
442 foreach (var tab in distributableTabs)
445 switch (tab.AddFiltered(post))
447 case MyCommon.HITRESULT.Copy:
450 case MyCommon.HITRESULT.CopyAndMark:
454 case MyCommon.HITRESULT.Move:
458 case MyCommon.HITRESULT.None:
460 case MyCommon.HITRESULT.Exclude:
461 if (tab.TabType == MyCommon.TabUsageType.Mentions)
462 excludedReply = true;
467 post.FilterHit = filterHit;
469 post.IsExcludeReply = excludedReply;
471 // 移動されなかったらRecentに追加
473 homeTab.AddPostQueue(post);
475 // 除外ルール適用のないReplyならReplyタブに追加
476 if (post.IsReply && !excludedReply)
477 replyTab.AddPostQueue(post);
479 // Fav済み発言だったらFavoritesタブに追加
481 favTab.AddPostQueue(post);
490 public void AddPost(PostClass Item)
492 Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
496 if (this.IsMuted(Item, isHomeTimeline: true))
499 if (Posts.TryGetValue(Item.StatusId, out var status))
503 if (Item.RetweetedId == null)
514 return; //追加済みなら何もしない
519 if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
522 if (Item.RetweetedId != null && SettingManager.Common.HideDuplicatedRetweets)
524 var retweetCount = this.UpdateRetweetCount(Item);
526 if (retweetCount > 1 && !Item.IsMe)
530 if (BlockIds.Contains(Item.UserId))
533 Posts.TryAdd(Item.StatusId, Item);
535 if (Item.IsFav && this.retweetsCount.ContainsKey(Item.StatusId))
537 return; //Fav済みのRetweet元発言は追加しない
539 this.addQueue.Enqueue(Item.StatusId);
543 public bool IsMuted(PostClass post, bool isHomeTimeline)
545 var muteTab = this.MuteTab;
546 if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
549 // これ以降は Twitter 標準のミュート機能に準じた判定
550 // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
552 // ホームタイムライン以外 (検索・リストなど) は対象外
560 if (this.MuteUserIds.Contains(post.UserId))
563 if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
569 private int UpdateRetweetCount(PostClass retweetPost)
571 if (retweetPost.RetweetedId == null)
572 throw new InvalidOperationException();
574 var retweetedId = retweetPost.RetweetedId.Value;
576 return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1);
579 public bool AddQuoteTweet(PostClass item)
583 if (IsMuted(item, isHomeTimeline: false) || BlockIds.Contains(item.UserId))
586 _quotes[item.StatusId] = item;
592 /// 全てのタブを横断して既読状態を変更します
594 /// <param name="statusId">変更するツイートのID</param>
595 /// <param name="read">既読状態</param>
596 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
597 public bool SetReadAllTab(long statusId, bool read)
601 foreach (var tab in this.Tabs)
603 if (!tab.Contains(statusId))
606 tab.SetReadState(statusId, read);
609 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
610 if (this.Posts.TryGetValue(statusId, out var post))
618 /// Home タブのツイートを全て既読にします。
619 /// ただし IsReply または FilterHit が true なものを除きます。
621 public void SetReadHomeTab()
623 var homeTab = this.HomeTab;
627 foreach (var statusId in homeTab.GetUnreadIds())
629 if (!this.Posts.TryGetValue(statusId, out var post))
632 if (post.IsReply || post.FilterHit)
635 this.SetReadAllTab(post.StatusId, read: true);
640 public PostClass? this[long ID]
644 if (this.Posts.TryGetValue(ID, out var status))
647 if (this._quotes.TryGetValue(ID, out status))
650 return this.GetTabsInnerStorageType()
651 .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
652 .FirstOrDefault(x => x != null);
656 public bool ContainsKey(long Id)
661 return Posts.ContainsKey(Id);
665 public void RenameTab(string Original, string NewName)
669 var index = this.Tabs.IndexOf(Original);
670 var tb = this.Tabs[Original];
671 this.tabs.RemoveAt(index);
672 tb.TabName = NewName;
673 this.tabs.Insert(index, tb);
677 public void FilterAll()
681 var homeTab = this.HomeTab;
682 var detachedIdsAll = Enumerable.Empty<long>();
684 foreach (var tab in this.Tabs.OfType<FilterTabModel>().ToArray())
686 // フィルタに変更のあったタブのみを対象とする
687 if (!tab.FilterModified)
690 tab.FilterModified = false;
692 // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
693 var orgIds = tab.StatusIds;
696 foreach (var post in Posts.Values)
698 var filterHit = false; // フィルタにヒットしたタブがあるか
699 var mark = false; // フィルタによってマーク付けされたか
700 var excluded = false; // 除外フィルタによって除外されたか
701 var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
703 switch (tab.AddFiltered(post, immediately: true))
705 case MyCommon.HITRESULT.Copy:
708 case MyCommon.HITRESULT.CopyAndMark:
712 case MyCommon.HITRESULT.Move:
716 case MyCommon.HITRESULT.None:
718 case MyCommon.HITRESULT.Exclude:
723 post.FilterHit = filterHit;
728 homeTab.RemovePostImmediately(post.StatusId);
730 if (tab.TabType == MyCommon.TabUsageType.Mentions)
732 post.IsExcludeReply = excluded;
734 // 除外ルール適用のないReplyならReplyタブに追加
735 if (post.IsReply && !excluded)
736 tab.AddPostImmediately(post.StatusId, post.IsRead);
740 // フィルタの更新によってタブから取り除かれたツイートのID
741 var detachedIds = orgIds.Except(tab.StatusIds).ToArray();
743 detachedIdsAll = detachedIdsAll.Concat(detachedIds);
746 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
747 foreach (var id in detachedIdsAll)
750 foreach (var tbTemp in this.Tabs.ToArray())
752 if (!tbTemp.IsDistributableTabType)
755 if (tbTemp.Contains(id))
764 if (this.Posts.TryGetValue(id, out var post))
765 homeTab.AddPostImmediately(post.StatusId, post.IsRead);
771 public void ClearTabIds(string TabName)
776 var tb = this.Tabs[TabName];
777 if (!tb.IsInnerStorageTabType)
779 foreach (var Id in tb.StatusIds)
782 foreach (var tab in this.Tabs)
784 if (tab.Contains(Id))
791 Posts.TryRemove(Id, out var removedPost);
800 public void RefreshOwl(ISet<long> follower)
804 if (follower.Count > 0)
806 foreach (var post in Posts.Values)
808 //if (post.UserId = 0 || post.IsDm) Continue For
815 post.IsOwl = !follower.Contains(post.UserId);
821 foreach (var post in Posts.Values)
829 public HomeTabModel HomeTab
830 => this.GetTabByType<HomeTabModel>()!;
832 public DirectMessagesTabModel DirectMessageTab
833 => this.GetTabByType<DirectMessagesTabModel>()!;
835 public MentionsTabModel MentionTab
836 => this.GetTabByType<MentionsTabModel>()!;
838 public FavoritesTabModel FavoriteTab
839 => this.GetTabByType<FavoritesTabModel>()!;
841 public TabModel? GetTabByType(MyCommon.TabUsageType tabType)
843 //Home,Mentions,DM,Favは1つに制限する
844 //その他のタイプを指定されたら、最初に合致したものを返す
848 return this.Tabs.FirstOrDefault(x => x.TabType.HasFlag(tabType));
852 public T? GetTabByType<T>() where T : TabModel
855 return this.Tabs.OfType<T>().FirstOrDefault();
858 public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType)
863 .Where(x => x.TabType.HasFlag(tabType))
868 public T[] GetTabsByType<T>() where T : TabModel
871 return this.Tabs.OfType<T>().ToArray();
874 public TabModel[] GetTabsInnerStorageType()
879 .Where(x => x.IsInnerStorageTabType)
884 public TabModel? GetTabByName(string tabName)
888 return this.Tabs.TryGetValue(tabName, out var tab)