OSDN Git Service

21e14778d81ca2782baa0c9fb2e585a529aef30c
[opentween/open-tween.git] / OpenTween / Models / TabInformations.cs
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.
10 //
11 // This file is part of OpenTween.
12 //
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)
16 // any later version.
17 //
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
21 // for more details.
22 //
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.
27
28 using System;
29 using System.Collections.Concurrent;
30 using System.Collections.Generic;
31 using System.Diagnostics;
32 using System.Linq;
33 using System.Text;
34 using System.Threading.Tasks;
35 using System.Windows.Forms;
36 using OpenTween.Setting;
37
38 namespace OpenTween.Models
39 {
40     public sealed class TabInformations
41     {
42         //個別タブの情報をDictionaryで保持
43         public IReadOnlyTabCollection Tabs
44             => this.tabs;
45
46         public MuteTabModel MuteTab { get; private set; }
47
48         public ConcurrentDictionary<long, PostClass> Posts { get; } = new ConcurrentDictionary<long, PostClass>();
49
50         private readonly Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
51         private readonly ConcurrentDictionary<long, int> retweetsCount = new ConcurrentDictionary<long, int>();
52
53         public Stack<TabModel> RemovedTab { get; } = new Stack<TabModel>();
54
55         public ISet<long> BlockIds { get; set; } = new HashSet<long>();
56         public ISet<long> MuteUserIds { get; set; } = new HashSet<long>();
57
58         //発言の追加
59         //AddPost(複数回) -> DistributePosts          -> SubmitUpdate
60
61         private readonly TabCollection tabs = new TabCollection();
62         private readonly ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
63
64         /// <summary>通知サウンドを再生する優先順位</summary>
65         private readonly Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
66         {
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,
72         };
73
74         //トランザクション用
75         private readonly object LockObj = new object();
76
77         private static readonly TabInformations _instance = new TabInformations();
78
79         //List
80         private List<ListElement> _lists = new List<ListElement>();
81
82         private TabInformations()
83         {
84         }
85
86         public static TabInformations GetInstance()
87             => _instance; // singleton
88
89         public string SelectedTabName { get; private set; } = "";
90
91         public TabModel SelectedTab
92             => this.Tabs[this.SelectedTabName];
93
94         public int SelectedTabIndex
95             => this.Tabs.IndexOf(this.SelectedTabName);
96
97         public List<ListElement> SubscribableLists
98         {
99             get => this._lists;
100             set
101             {
102                 if (value != null && value.Count > 0)
103                 {
104                     foreach (var tb in this.GetTabsByType<ListTimelineTabModel>())
105                     {
106                         foreach (var list in value)
107                         {
108                             if (tb.ListInfo.Id == list.Id)
109                             {
110                                 tb.ListInfo = list;
111                                 break;
112                             }
113                         }
114                     }
115                 }
116                 _lists = value;
117             }
118         }
119
120         public bool AddTab(TabModel tab)
121         {
122             lock (this.LockObj)
123             {
124                 if (tab is MuteTabModel muteTab)
125                 {
126                     if (this.MuteTab != null)
127                         return false;
128
129                     this.MuteTab = muteTab;
130                     return true;
131                 }
132
133                 if (this.Tabs.Contains(tab.TabName))
134                     return false;
135
136                 this.tabs.Add(tab);
137                 tab.SetSortMode(this.SortMode, this.SortOrder);
138
139                 return true;
140             }
141         }
142
143         //public void AddTab(string TabName, TabClass Tab)
144         //{
145         //    _tabs.Add(TabName, Tab);
146         //}
147
148         public void RemoveTab(string TabName)
149         {
150             lock (LockObj)
151             {
152                 var tb = GetTabByName(TabName);
153                 if (tb.IsDefaultTabType) return; //念のため
154
155                 if (!tb.IsInnerStorageTabType)
156                 {
157                     var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
158                     var dmTab = GetTabByType(MyCommon.TabUsageType.DirectMessage);
159
160                     for (var idx = 0; idx < tb.AllCount; ++idx)
161                     {
162                         var exist = false;
163                         var Id = tb.GetStatusIdAt(idx);
164                         if (Id < 0) continue;
165                         foreach (var tab in this.Tabs)
166                         {
167                             if (tab != tb && tab != dmTab)
168                             {
169                                 if (tab.Contains(Id))
170                                 {
171                                     exist = true;
172                                     break;
173                                 }
174                             }
175                         }
176                         if (!exist) homeTab.AddPostImmediately(Id, this.Posts[Id].IsRead);
177                     }
178                 }
179                 this.RemovedTab.Push(tb);
180                 this.tabs.Remove(TabName);
181             }
182         }
183
184         public void ReplaceTab(TabModel tab)
185         {
186             if (!this.ContainsTab(tab.TabName))
187                 throw new ArgumentOutOfRangeException(nameof(tab));
188
189             var index = this.tabs.IndexOf(tab);
190             this.tabs.RemoveAt(index);
191             this.tabs.Insert(index, tab);
192         }
193
194         public void MoveTab(int newIndex, TabModel tab)
195         {
196             if (!this.ContainsTab(tab))
197                 throw new ArgumentOutOfRangeException(nameof(tab));
198
199             this.tabs.Remove(tab);
200             this.tabs.Insert(newIndex, tab);
201         }
202
203         public bool ContainsTab(string TabText)
204             => this.Tabs.Contains(TabText);
205
206         public bool ContainsTab(TabModel ts)
207             => this.Tabs.Contains(ts);
208
209         public void SelectTab(string tabName)
210         {
211             if (!this.Tabs.Contains(tabName))
212                 throw new ArgumentException($"{tabName} does not exist.", nameof(tabName));
213
214             this.SelectedTabName = tabName;
215         }
216
217         /// <summary>
218         /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
219         /// </summary>
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);
225
226         /// <summary>
227         /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
228         /// </summary>
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)
234         {
235             if (!this.ContainsTab(baseTabName))
236                 return baseTabName;
237
238             foreach (var i in Enumerable.Range(2, retryCount - 1))
239             {
240                 var tabName = baseTabName + i;
241                 if (!this.ContainsTab(tabName))
242                 {
243                     return tabName;
244                 }
245             }
246
247             var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
248             throw new TabException(message);
249         }
250
251         public SortOrder SortOrder { get; private set; }
252
253         public ComparerMode SortMode { get; private set; }
254
255         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
256         {
257             this.SortMode = mode;
258             this.SortOrder = sortOrder;
259
260             foreach (var tab in this.Tabs)
261                 tab.SetSortMode(mode, sortOrder);
262         }
263
264         public SortOrder ToggleSortOrder(ComparerMode sortMode)
265         {
266             var sortOrder = this.SortOrder;
267
268             if (this.SortMode == sortMode)
269             {
270                 if (sortOrder == SortOrder.Ascending)
271                     sortOrder = SortOrder.Descending;
272                 else
273                     sortOrder = SortOrder.Ascending;
274             }
275             else
276             {
277                 sortOrder = SortOrder.Ascending;
278             }
279
280             this.SetSortMode(sortMode, sortOrder);
281
282             return this.SortOrder;
283         }
284
285         //    public PostClass RetweetSource(long Id)
286         //    {
287         //        get
288         //        {
289         //            if (_retweets.ContainsKey(Id))
290         //            {
291         //                return _retweets[Id];
292         //            }
293         //            else
294         //            {
295         //                return null;
296         //            }
297         //        }
298         //    }
299         public PostClass RetweetSource(long Id)
300             => this.Posts.TryGetValue(Id, out var status) ? status : null;
301
302         public void ScrubGeoReserve(long id, long upToStatusId)
303         {
304             lock (LockObj)
305             {
306                 //this._scrubGeo.Add(new ScrubGeoInfo With {.UserId = id, .UpToStatusId = upToStatusId});
307                 this.ScrubGeo(id, upToStatusId);
308             }
309         }
310
311         private void ScrubGeo(long userId, long upToStatusId)
312         {
313             lock (LockObj)
314             {
315                 var userPosts = from post in this.Posts.Values
316                                 where post.UserId == userId && post.UserId <= upToStatusId
317                                 select post;
318
319                 foreach (var p in userPosts)
320                 {
321                     p.PostGeo = null;
322                 }
323
324                 var userPosts2 = from tb in this.GetTabsInnerStorageType()
325                                  from post in tb.Posts.Values
326                                  where post.UserId == userId && post.UserId <= upToStatusId
327                                  select post;
328
329                 foreach (var p in userPosts2)
330                 {
331                     p.PostGeo = null;
332                 }
333             }
334         }
335
336         public void RemovePostFromAllTabs(long statusId, bool setIsDeleted)
337         {
338             foreach (var tab in this.Tabs)
339             {
340                 tab.EnqueueRemovePost(statusId, setIsDeleted);
341             }
342
343             if (setIsDeleted)
344             {
345                 if (this.Posts.TryGetValue(statusId, out var post))
346                     post.IsDeleted = true;
347             }
348         }
349
350         public int SubmitUpdate()
351             => this.SubmitUpdate(out var soundFile, out var notifyPosts, out var newMentionOrDm, out var isDeletePost);
352
353         public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
354             out bool newMentionOrDm, out bool isDeletePost)
355         {
356             // 注:メインスレッドから呼ぶこと
357             lock (this.LockObj)
358             {
359                 soundFile = "";
360                 notifyPosts = Array.Empty<PostClass>();
361                 newMentionOrDm = false;
362                 isDeletePost = false;
363
364                 var addedCountTotal = 0;
365                 var removedIdsAll = new List<long>();
366                 var notifyPostsList = new List<PostClass>();
367
368                 var currentNotifyPriority = -1;
369
370                 foreach (var tab in this.Tabs)
371                 {
372                     // 振分確定 (各タブに反映)
373                     var addedIds = tab.AddSubmit();
374
375                     if (tab.TabType == MyCommon.TabUsageType.Mentions ||
376                         tab.TabType == MyCommon.TabUsageType.DirectMessage)
377                     {
378                         if (addedIds.Count > 0)
379                             newMentionOrDm = true;
380                     }
381
382                     if (addedIds.Count != 0)
383                     {
384                         if (tab.Notify)
385                         {
386                             // 通知対象のリストに追加
387                             foreach (var statusId in addedIds)
388                             {
389                                 if (tab.Posts.TryGetValue(statusId, out var post))
390                                     notifyPostsList.Add(post);
391                             }
392                         }
393
394                         // 通知サウンドは TabClass.Notify の値に関わらず鳴らす
395                         // SettingCommon.PlaySound が false であれば TweenMain 側で無効化される
396                         if (!string.IsNullOrEmpty(tab.SoundFile))
397                         {
398                             if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out var notifyPriority))
399                                 notifyPriority = 0;
400
401                             if (notifyPriority > currentNotifyPriority)
402                             {
403                                 // より優先度の高い通知を再生する
404                                 soundFile = tab.SoundFile;
405                                 currentNotifyPriority = notifyPriority;
406                             }
407                         }
408                     }
409
410                     addedCountTotal += addedIds.Count;
411
412                     var removedIds = tab.RemoveSubmit();
413                     removedIdsAll.AddRange(removedIds);
414                 }
415
416                 notifyPosts = notifyPostsList.Distinct().ToArray();
417
418                 if (removedIdsAll.Count > 0)
419                     isDeletePost = true;
420
421                 foreach (var removedId in removedIdsAll.Distinct())
422                 {
423                     var orphaned = true;
424                     foreach (var tab in this.Tabs)
425                     {
426                         if (tab.Contains(removedId))
427                         {
428                             orphaned = false;
429                             break;
430                         }
431                     }
432
433                     // 全てのタブから表示されなくなった発言は this._statuses からも削除する
434                     if (orphaned)
435                         this.Posts.TryRemove(removedId, out var removedPost);
436                 }
437
438                 return addedCountTotal;
439             }
440         }
441
442         public int DistributePosts()
443         {
444             lock (this.LockObj)
445             {
446                 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
447                 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
448                 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
449
450                 var distributableTabs = this.GetTabsByType<FilterTabModel>()
451                     .ToArray();
452
453                 var adddedCount = 0;
454
455                 while (this.addQueue.TryDequeue(out var statusId))
456                 {
457                     if (!this.Posts.TryGetValue(statusId, out var post))
458                         continue;
459
460                     var filterHit = false; // フィルタにヒットしたタブがあるか
461                     var mark = false; // フィルタによってマーク付けされたか
462                     var excludedReply = false; // リプライから除外されたか
463                     var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
464
465                     foreach (var tab in distributableTabs)
466                     {
467                         // 各振り分けタブのフィルタを実行する
468                         switch (tab.AddFiltered(post))
469                         {
470                             case MyCommon.HITRESULT.Copy:
471                                 filterHit = true;
472                                 break;
473                             case MyCommon.HITRESULT.CopyAndMark:
474                                 filterHit = true;
475                                 mark = true;
476                                 break;
477                             case MyCommon.HITRESULT.Move:
478                                 filterHit = true;
479                                 moved = true;
480                                 break;
481                             case MyCommon.HITRESULT.None:
482                                 break;
483                             case MyCommon.HITRESULT.Exclude:
484                                 if (tab.TabType == MyCommon.TabUsageType.Mentions)
485                                     excludedReply = true;
486                                 break;
487                         }
488                     }
489
490                     post.FilterHit = filterHit;
491                     post.IsMark = mark;
492                     post.IsExcludeReply = excludedReply;
493
494                     // 移動されなかったらRecentに追加
495                     if (!moved)
496                         homeTab.AddPostQueue(post);
497
498                     // 除外ルール適用のないReplyならReplyタブに追加
499                     if (post.IsReply && !excludedReply)
500                         replyTab.AddPostQueue(post);
501
502                     // Fav済み発言だったらFavoritesタブに追加
503                     if (post.IsFav)
504                         favTab.AddPostQueue(post);
505
506                     adddedCount++;
507                 }
508
509                 return adddedCount;
510             }
511         }
512
513         public void AddPost(PostClass Item)
514         {
515             Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
516
517             lock (LockObj)
518             {
519                 if (this.IsMuted(Item, isHomeTimeline: true))
520                     return;
521
522                 if (Posts.TryGetValue(Item.StatusId, out var status))
523                 {
524                     if (Item.IsFav)
525                     {
526                         if (Item.RetweetedId == null)
527                         {
528                             status.IsFav = true;
529                         }
530                         else
531                         {
532                             Item.IsFav = false;
533                         }
534                     }
535                     else
536                     {
537                         return;        //追加済みなら何もしない
538                     }
539                 }
540                 else
541                 {
542                     if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
543
544                     //既に持っている公式RTは捨てる
545                     if (Item.RetweetedId != null && SettingManager.Common.HideDuplicatedRetweets)
546                     {
547                         var retweetCount = this.UpdateRetweetCount(Item);
548
549                         if (retweetCount > 1 && !Item.IsMe)
550                             return;
551                     }
552
553                     if (BlockIds.Contains(Item.UserId))
554                         return;
555
556                     Posts.TryAdd(Item.StatusId, Item);
557                 }
558                 if (Item.IsFav && this.retweetsCount.ContainsKey(Item.StatusId))
559                 {
560                     return;    //Fav済みのRetweet元発言は追加しない
561                 }
562                 this.addQueue.Enqueue(Item.StatusId);
563             }
564         }
565
566         public bool IsMuted(PostClass post, bool isHomeTimeline)
567         {
568             var muteTab = this.MuteTab;
569             if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
570                 return true;
571
572             // これ以降は Twitter 標準のミュート機能に準じた判定
573             // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
574
575             // ホームタイムライン以外 (検索・リストなど) は対象外
576             if (!isHomeTimeline)
577                 return false;
578
579             // リプライはミュート対象外
580             if (post.IsReply)
581                 return false;
582
583             if (this.MuteUserIds.Contains(post.UserId))
584                 return true;
585
586             if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
587                 return true;
588
589             return false;
590         }
591
592         private int UpdateRetweetCount(PostClass retweetPost)
593         {
594             var retweetedId = retweetPost.RetweetedId.Value;
595
596             return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1);
597         }
598
599         public bool AddQuoteTweet(PostClass item)
600         {
601             lock (LockObj)
602             {
603                 if (IsMuted(item, isHomeTimeline: false) || BlockIds.Contains(item.UserId))
604                     return false;
605
606                 _quotes[item.StatusId] = item;
607                 return true;
608             }
609         }
610
611         /// <summary>
612         /// 全てのタブを横断して既読状態を変更します
613         /// </summary>
614         /// <param name="statusId">変更するツイートのID</param>
615         /// <param name="read">既読状態</param>
616         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
617         public bool SetReadAllTab(long statusId, bool read)
618         {
619             lock (LockObj)
620             {
621                 foreach (var tab in this.Tabs)
622                 {
623                     if (!tab.Contains(statusId))
624                         continue;
625
626                     tab.SetReadState(statusId, read);
627                 }
628
629                 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
630                 if (this.Posts.TryGetValue(statusId, out var post))
631                     post.IsRead = read;
632
633                 return true;
634             }
635         }
636
637         /// <summary>
638         /// Home タブのツイートを全て既読にします。
639         /// ただし IsReply または FilterHit が true なものを除きます。
640         /// </summary>
641         public void SetReadHomeTab()
642         {
643             var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
644
645             lock (LockObj)
646             {
647                 foreach (var statusId in homeTab.GetUnreadIds())
648                 {
649                     if (!this.Posts.TryGetValue(statusId, out var post))
650                         continue;
651
652                     if (post.IsReply || post.FilterHit)
653                         continue;
654
655                     this.SetReadAllTab(post.StatusId, read: true);
656                 }
657             }
658         }
659
660         public PostClass this[long ID]
661         {
662             get
663             {
664                 if (this.Posts.TryGetValue(ID, out var status))
665                     return status;
666
667                 if (this._quotes.TryGetValue(ID, out status))
668                     return status;
669
670                 return this.GetTabsInnerStorageType()
671                     .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
672                     .FirstOrDefault(x => x != null);
673             }
674         }
675
676         public bool ContainsKey(long Id)
677         {
678             //DM,公式検索は非対応
679             lock (LockObj)
680             {
681                 return Posts.ContainsKey(Id);
682             }
683         }
684
685         public void RenameTab(string Original, string NewName)
686         {
687             lock (this.LockObj)
688             {
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);
694             }
695         }
696
697         public void FilterAll()
698         {
699             lock (LockObj)
700             {
701                 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
702                 var detachedIdsAll = Enumerable.Empty<long>();
703
704                 foreach (var tab in this.Tabs.OfType<FilterTabModel>().ToArray())
705                 {
706                     // フィルタに変更のあったタブのみを対象とする
707                     if (!tab.FilterModified)
708                         continue;
709
710                     tab.FilterModified = false;
711
712                     // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
713                     var orgIds = tab.StatusIds;
714                     tab.ClearIDs();
715
716                     foreach (var post in Posts.Values)
717                     {
718                         var filterHit = false; // フィルタにヒットしたタブがあるか
719                         var mark = false; // フィルタによってマーク付けされたか
720                         var excluded = false; // 除外フィルタによって除外されたか
721                         var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
722
723                         switch (tab.AddFiltered(post, immediately: true))
724                         {
725                             case MyCommon.HITRESULT.Copy:
726                                 filterHit = true;
727                                 break;
728                             case MyCommon.HITRESULT.CopyAndMark:
729                                 filterHit = true;
730                                 mark = true;
731                                 break;
732                             case MyCommon.HITRESULT.Move:
733                                 filterHit = true;
734                                 moved = true;
735                                 break;
736                             case MyCommon.HITRESULT.None:
737                                 break;
738                             case MyCommon.HITRESULT.Exclude:
739                                 excluded = true;
740                                 break;
741                         }
742
743                         post.FilterHit = filterHit;
744                         post.IsMark = mark;
745
746                         // 移動されたらRecentから除去
747                         if (moved)
748                             homeTab.RemovePostImmediately(post.StatusId);
749
750                         if (tab.TabType == MyCommon.TabUsageType.Mentions)
751                         {
752                             post.IsExcludeReply = excluded;
753
754                             // 除外ルール適用のないReplyならReplyタブに追加
755                             if (post.IsReply && !excluded)
756                                 tab.AddPostImmediately(post.StatusId, post.IsRead);
757                         }
758                     }
759
760                     // フィルタの更新によってタブから取り除かれたツイートのID
761                     var detachedIds = orgIds.Except(tab.StatusIds).ToArray();
762
763                     detachedIdsAll = detachedIdsAll.Concat(detachedIds);
764                 }
765
766                 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
767                 foreach (var id in detachedIdsAll)
768                 {
769                     var hit = false;
770                     foreach (var tbTemp in this.Tabs.ToArray())
771                     {
772                         if (!tbTemp.IsDistributableTabType)
773                             continue;
774
775                         if (tbTemp.Contains(id))
776                         {
777                             hit = true;
778                             break;
779                         }
780                     }
781
782                     if (!hit)
783                     {
784                         if (this.Posts.TryGetValue(id, out var post))
785                             homeTab.AddPostImmediately(post.StatusId, post.IsRead);
786                     }
787                 }
788             }
789         }
790
791         public void ClearTabIds(string TabName)
792         {
793             //不要なPostを削除
794             lock (LockObj)
795             {
796                 var tb = this.Tabs[TabName];
797                 if (!tb.IsInnerStorageTabType)
798                 {
799                     foreach (var Id in tb.StatusIds)
800                     {
801                         var Hit = false;
802                         foreach (var tab in this.Tabs)
803                         {
804                             if (tab.Contains(Id))
805                             {
806                                 Hit = true;
807                                 break;
808                             }
809                         }
810                         if (!Hit)
811                             Posts.TryRemove(Id, out var removedPost);
812                     }
813                 }
814
815                 //指定タブをクリア
816                 tb.ClearIDs();
817             }
818         }
819
820         public void RefreshOwl(ISet<long> follower)
821         {
822             lock (LockObj)
823             {
824                 if (follower.Count > 0)
825                 {
826                     foreach (var post in Posts.Values)
827                     {
828                         //if (post.UserId = 0 || post.IsDm) Continue For
829                         if (post.IsMe)
830                         {
831                             post.IsOwl = false;
832                         }
833                         else
834                         {
835                             post.IsOwl = !follower.Contains(post.UserId);
836                         }
837                     }
838                 }
839                 else
840                 {
841                     foreach (var post in Posts.Values)
842                     {
843                         post.IsOwl = false;
844                     }
845                 }
846             }
847         }
848
849         public TabModel GetTabByType(MyCommon.TabUsageType tabType)
850         {
851             //Home,Mentions,DM,Favは1つに制限する
852             //その他のタイプを指定されたら、最初に合致したものを返す
853             //合致しなければnullを返す
854             lock (LockObj)
855             {
856                 return this.Tabs.FirstOrDefault(x => x.TabType.HasFlag(tabType));
857             }
858         }
859
860         public T GetTabByType<T>() where T : TabModel
861         {
862             lock (this.LockObj)
863                 return this.Tabs.OfType<T>().FirstOrDefault();
864         }
865
866         public TabModel[] GetTabsByType(MyCommon.TabUsageType tabType)
867         {
868             lock (LockObj)
869             {
870                 return this.Tabs
871                     .Where(x => x.TabType.HasFlag(tabType))
872                     .ToArray();
873             }
874         }
875
876         public T[] GetTabsByType<T>() where T : TabModel
877         {
878             lock (this.LockObj)
879                 return this.Tabs.OfType<T>().ToArray();
880         }
881
882         public TabModel[] GetTabsInnerStorageType()
883         {
884             lock (LockObj)
885             {
886                 return this.Tabs
887                     .Where(x => x.IsInnerStorageTabType)
888                     .ToArray();
889             }
890         }
891
892         public TabModel GetTabByName(string tabName)
893         {
894             lock (LockObj)
895             {
896                 return this.Tabs.TryGetValue(tabName, out var tab)
897                     ? tab
898                     : null;
899             }
900         }
901     }
902 }