OSDN Git Service

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