OSDN Git Service

TabClass.AddSubmitメソッドからisMentionIncluded引数を削除
[opentween/open-tween.git] / OpenTween / StatusDictionary.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.ComponentModel;
32 using System.Diagnostics;
33 using System.Linq;
34 using System.Linq.Expressions;
35 using System.Net;
36 using System.Reflection;
37 using System.Text;
38 using System.Text.RegularExpressions;
39 using System.Threading;
40 using System.Windows.Forms;
41 using System.Xml.Serialization;
42
43 namespace OpenTween
44 {
45     public class PostClass : ICloneable
46     {
47         public struct StatusGeo : IEquatable<StatusGeo>
48         {
49             public double Longitude { get; }
50             public double Latitude { get; }
51
52             public StatusGeo(double longitude, double latitude)
53             {
54                 this.Longitude = longitude;
55                 this.Latitude = latitude;
56             }
57
58             public override int GetHashCode()
59                 => this.Longitude.GetHashCode() ^ this.Latitude.GetHashCode();
60
61             public override bool Equals(object obj)
62                 => obj is StatusGeo ? this.Equals((StatusGeo)obj) : false;
63
64             public bool Equals(StatusGeo other)
65                 => this.Longitude == other.Longitude && this.Latitude == other.Longitude;
66
67             public static bool operator ==(StatusGeo left, StatusGeo right)
68                 => left.Equals(right);
69
70             public static bool operator !=(StatusGeo left, StatusGeo right)
71                 => !left.Equals(right);
72         }
73         public string Nickname { get; set; }
74         public string TextFromApi { get; set; }
75         public string ImageUrl { get; set; }
76         public string ScreenName { get; set; }
77         public DateTime CreatedAt { get; set; }
78         public long StatusId { get; set; }
79         private bool _IsFav;
80         public string Text { get; set; }
81         public bool IsRead { get; set; }
82         public bool IsReply { get; set; }
83         public bool IsExcludeReply { get; set; }
84         private bool _IsProtect;
85         public bool IsOwl { get; set; }
86         private bool _IsMark;
87         public string InReplyToUser { get; set; }
88         private long? _InReplyToStatusId;
89         public string Source { get; set; }
90         public Uri SourceUri { get; set; }
91         public List<string> ReplyToList { get; set; }
92         public bool IsMe { get; set; }
93         public bool IsDm { get; set; }
94         public long UserId { get; set; }
95         public bool FilterHit { get; set; }
96         public string RetweetedBy { get; set; }
97         public long? RetweetedId { get; set; }
98         private bool _IsDeleted = false;
99         private StatusGeo? _postGeo = null;
100         public int RetweetedCount { get; set; }
101         public long? RetweetedByUserId { get; set; }
102         public long? InReplyToUserId { get; set; }
103         public List<MediaInfo> Media { get; set; }
104         public long[] QuoteStatusIds { get; set; }
105
106         public int FavoritedCount { get; set; }
107
108         private States _states = States.None;
109
110         [Flags]
111         private enum States
112         {
113             None = 0,
114             Protect = 1,
115             Mark = 2,
116             Reply = 4,
117             Geo = 8,
118         }
119
120         public PostClass()
121         {
122             RetweetedBy = "";
123             Media = new List<MediaInfo>();
124             ReplyToList = new List<string>();
125             QuoteStatusIds = new long[0];
126         }
127
128         public string TextSingleLine
129         {
130             get
131             {
132                 return this.TextFromApi == null ? null : this.TextFromApi.Replace("\n", " ");
133             }
134         }
135
136         public bool IsFav
137         {
138             get
139             {
140                 if (this.RetweetedId != null)
141                 {
142                     var post = this.RetweetSource;
143                     if (post != null)
144                     {
145                         return post.IsFav;
146                     }
147                 }
148
149                 return _IsFav;
150             }
151             set
152             {
153                 _IsFav = value;
154                 if (this.RetweetedId != null)
155                 {
156                     var post = this.RetweetSource;
157                     if (post != null)
158                     {
159                         post.IsFav = value;
160                     }
161                 }
162             }
163         }
164
165         public bool IsProtect
166         {
167             get
168             {
169                 return _IsProtect;
170             }
171             set
172             {
173                 if (value)
174                 {
175                     _states = _states | States.Protect;
176                 }
177                 else
178                 {
179                     _states = _states & ~States.Protect;
180                 }
181                 _IsProtect = value;
182             }
183         }
184         public bool IsMark
185         {
186             get
187             {
188                 return _IsMark;
189             }
190             set
191             {
192                 if (value)
193                 {
194                     _states = _states | States.Mark;
195                 }
196                 else
197                 {
198                     _states = _states & ~States.Mark;
199                 }
200                 _IsMark = value;
201             }
202         }
203         public long? InReplyToStatusId
204         {
205             get
206             {
207                 return _InReplyToStatusId;
208             }
209             set
210             {
211                 if (value != null)
212                 {
213                     _states = _states | States.Reply;
214                 }
215                 else
216                 {
217                     _states = _states & ~States.Reply;
218                 }
219                 _InReplyToStatusId = value;
220             }
221         }
222
223         public bool IsDeleted
224         {
225             get
226             {
227                 return _IsDeleted;
228             }
229             set
230             {
231                 if (value)
232                 {
233                     this.InReplyToStatusId = null;
234                     this.InReplyToUser = "";
235                     this.InReplyToUserId = null;
236                     this.IsReply = false;
237                     this.ReplyToList = new List<string>();
238                     this._states = States.None;
239                 }
240                 _IsDeleted = value;
241             }
242         }
243
244         protected virtual PostClass RetweetSource
245         {
246             get
247             {
248                 return TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value);
249             }
250         }
251
252         public StatusGeo? PostGeo
253         {
254             get
255             {
256                 return _postGeo;
257             }
258             set
259             {
260                 if (value != null)
261                 {
262                     _states |= States.Geo;
263                 }
264                 else
265                 {
266                     _states &= ~States.Geo;
267                 }
268                 _postGeo = value;
269             }
270         }
271
272         public int StateIndex
273         {
274             get
275             {
276                 return (int)_states - 1;
277             }
278         }
279
280         // 互換性のために用意
281         public string SourceHtml
282         {
283             get
284             {
285                 if (this.SourceUri == null)
286                     return WebUtility.HtmlEncode(this.Source);
287
288                 return string.Format("<a href=\"{0}\" rel=\"nofollow\">{1}</a>",
289                     WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source));
290             }
291         }
292
293         /// <summary>
294         /// このツイートが指定したユーザーによって削除可能であるかを判定します
295         /// </summary>
296         /// <param name="selfUserId">ツイートを削除しようとするユーザーのID</param>
297         /// <returns>削除可能であれば true、そうでなければ false</returns>
298         public bool CanDeleteBy(long selfUserId)
299         {
300             // 自分が送った DM と自分に届いた DM のどちらも削除可能
301             if (this.IsDm)
302                 return true;
303
304             // 自分のツイート or 他人に RT された自分のツイート
305             if (this.UserId == selfUserId)
306                 return true;
307
308             // 自分が RT したツイート
309             if (this.RetweetedByUserId == selfUserId)
310                 return true;
311
312             return false;
313         }
314
315         public PostClass ConvertToOriginalPost()
316         {
317             if (this.RetweetedId == null)
318                 throw new InvalidOperationException();
319
320             var originalPost = this.Clone();
321
322             originalPost.StatusId = this.RetweetedId.Value;
323             originalPost.RetweetedId = null;
324             originalPost.RetweetedBy = "";
325             originalPost.RetweetedByUserId = null;
326             originalPost.RetweetedCount = 1;
327
328             return originalPost;
329         }
330
331         public PostClass Clone()
332         {
333             var clone = (PostClass)this.MemberwiseClone();
334             clone.ReplyToList = new List<string>(this.ReplyToList);
335             clone.Media = new List<MediaInfo>(this.Media);
336             clone.QuoteStatusIds = this.QuoteStatusIds.ToArray();
337
338             return clone;
339         }
340
341         object ICloneable.Clone()
342             => this.Clone();
343
344         public override bool Equals(object obj)
345         {
346             if (obj == null || this.GetType() != obj.GetType()) return false;
347             return this.Equals((PostClass)obj);
348         }
349
350         public bool Equals(PostClass other)
351         {
352             if (other == null) return false;
353             return (this.Nickname == other.Nickname) &&
354                     (this.TextFromApi == other.TextFromApi) &&
355                     (this.ImageUrl == other.ImageUrl) &&
356                     (this.ScreenName == other.ScreenName) &&
357                     (this.CreatedAt == other.CreatedAt) &&
358                     (this.StatusId == other.StatusId) &&
359                     (this.IsFav == other.IsFav) &&
360                     (this.Text == other.Text) &&
361                     (this.IsRead == other.IsRead) &&
362                     (this.IsReply == other.IsReply) &&
363                     (this.IsExcludeReply == other.IsExcludeReply) &&
364                     (this.IsProtect == other.IsProtect) &&
365                     (this.IsOwl == other.IsOwl) &&
366                     (this.IsMark == other.IsMark) &&
367                     (this.InReplyToUser == other.InReplyToUser) &&
368                     (this.InReplyToStatusId == other.InReplyToStatusId) &&
369                     (this.Source == other.Source) &&
370                     (this.SourceUri == other.SourceUri) &&
371                     (this.ReplyToList.SequenceEqual(other.ReplyToList)) &&
372                     (this.IsMe == other.IsMe) &&
373                     (this.IsDm == other.IsDm) &&
374                     (this.UserId == other.UserId) &&
375                     (this.FilterHit == other.FilterHit) &&
376                     (this.RetweetedBy == other.RetweetedBy) &&
377                     (this.RetweetedId == other.RetweetedId) &&
378                     (this.IsDeleted == other.IsDeleted) &&
379                     (this.InReplyToUserId == other.InReplyToUserId);
380
381         }
382
383         public override int GetHashCode()
384         {
385             return this.StatusId.GetHashCode();
386         }
387     }
388
389     public class MediaInfo
390     {
391         public string Url { get; }
392         public string VideoUrl { get; }
393
394         public MediaInfo(string url)
395             : this(url, null)
396         {
397         }
398
399         public MediaInfo(string url, string videoUrl)
400         {
401             this.Url = url;
402             this.VideoUrl = !string.IsNullOrEmpty(videoUrl) ? videoUrl : null;
403         }
404
405         public override bool Equals(object obj)
406         {
407             var info = obj as MediaInfo;
408             return info != null &&
409                 info.Url == this.Url &&
410                 info.VideoUrl == this.VideoUrl;
411         }
412
413         public override int GetHashCode()
414         {
415             return (this.Url == null ? 0 : this.Url.GetHashCode()) ^
416                    (this.VideoUrl == null ? 0 : this.VideoUrl.GetHashCode());
417         }
418
419         public override string ToString()
420         {
421             return this.Url;
422         }
423     }
424
425     public sealed class TabInformations
426     {
427         //個別タブの情報をDictionaryで保持
428         private Dictionary<string, TabClass> _tabs = new Dictionary<string, TabClass>();
429         private ConcurrentDictionary<long, PostClass> _statuses = new ConcurrentDictionary<long, PostClass>();
430         private Dictionary<long, PostClass> _retweets = new Dictionary<long, PostClass>();
431         private Dictionary<long, PostClass> _quotes = new Dictionary<long, PostClass>();
432         private Stack<TabClass> _removedTab = new Stack<TabClass>();
433
434         public ISet<long> BlockIds = new HashSet<long>();
435         public ISet<long> MuteUserIds = new HashSet<long>();
436
437         //発言の追加
438         //AddPost(複数回) -> DistributePosts          -> SubmitUpdate
439
440         private ConcurrentQueue<long> addQueue = new ConcurrentQueue<long>();
441         private ConcurrentQueue<long> deleteQueue = new ConcurrentQueue<long>();
442
443         /// <summary>通知サウンドを再生する優先順位</summary>
444         private Dictionary<MyCommon.TabUsageType, int> notifyPriorityByTabType = new Dictionary<MyCommon.TabUsageType, int>
445         {
446             [MyCommon.TabUsageType.DirectMessage] = 100,
447             [MyCommon.TabUsageType.Mentions] = 90,
448             [MyCommon.TabUsageType.UserDefined] = 80,
449             [MyCommon.TabUsageType.Home] = 70,
450             [MyCommon.TabUsageType.Favorites] = 60,
451         };
452
453         //トランザクション用
454         private readonly object LockObj = new object();
455
456         private static TabInformations _instance = new TabInformations();
457
458         //List
459         private List<ListElement> _lists = new List<ListElement>();
460
461         private TabInformations()
462         {
463         }
464
465         public static TabInformations GetInstance()
466         {
467             return _instance;    //singleton
468         }
469
470         public List<ListElement> SubscribableLists
471         {
472             get
473             {
474                 return _lists;
475             }
476             set
477             {
478                 if (value != null && value.Count > 0)
479                 {
480                     foreach (var tb in this.GetTabsByType(MyCommon.TabUsageType.Lists))
481                     {
482                         foreach (var list in value)
483                         {
484                             if (tb.ListInfo.Id == list.Id)
485                             {
486                                 tb.ListInfo = list;
487                                 break;
488                             }
489                         }
490                     }
491                 }
492                 _lists = value;
493             }
494         }
495
496         public bool AddTab(string TabName, MyCommon.TabUsageType TabType, ListElement List)
497         {
498             if (_tabs.ContainsKey(TabName)) return false;
499             var tb = new TabClass(TabName, TabType, List);
500             _tabs.Add(TabName, tb);
501             tb.SetSortMode(this.SortMode, this.SortOrder);
502             return true;
503         }
504
505         //public void AddTab(string TabName, TabClass Tab)
506         //{
507         //    _tabs.Add(TabName, Tab);
508         //}
509
510         public void RemoveTab(string TabName)
511         {
512             lock (LockObj)
513             {
514                 var tb = GetTabByName(TabName);
515                 if (tb.IsDefaultTabType) return; //念のため
516
517                 if (!tb.IsInnerStorageTabType)
518                 {
519                     var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
520                     var dmTab = GetTabByType(MyCommon.TabUsageType.DirectMessage);
521
522                     for (int idx = 0; idx < tb.AllCount; ++idx)
523                     {
524                         var exist = false;
525                         var Id = tb.GetId(idx);
526                         if (Id < 0) continue;
527                         foreach (var tab in _tabs.Values)
528                         {
529                             if (tab != tb && tab != dmTab)
530                             {
531                                 if (tab.Contains(Id))
532                                 {
533                                     exist = true;
534                                     break;
535                                 }
536                             }
537                         }
538                         if (!exist) homeTab.AddPostImmediately(Id, _statuses[Id].IsRead);
539                     }
540                 }
541                 _removedTab.Push(tb);
542                 _tabs.Remove(TabName);
543             }
544         }
545
546         public Stack<TabClass> RemovedTab
547         {
548             get { return _removedTab; }
549         }
550
551         public bool ContainsTab(string TabText)
552         {
553             return _tabs.ContainsKey(TabText);
554         }
555
556         public bool ContainsTab(TabClass ts)
557         {
558             return _tabs.ContainsValue(ts);
559         }
560
561         /// <summary>
562         /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
563         /// </summary>
564         /// <param name="baseTabName">作成したいタブ名</param>
565         /// <returns>生成されたタブ名</returns>
566         /// <exception cref="TabException">タブ名の生成を 100 回試行して失敗した場合</exception>
567         public string MakeTabName(string baseTabName)
568         {
569             return this.MakeTabName(baseTabName, 100);
570         }
571
572         /// <summary>
573         /// 指定されたタブ名を元に、既存のタブ名との重複を避けた名前を生成します
574         /// </summary>
575         /// <param name="baseTabName">作成したいタブ名</param>
576         /// <param name="retryCount">重複を避けたタブ名を生成する試行回数</param>
577         /// <returns>生成されたタブ名</returns>
578         /// <exception cref="TabException">retryCount で指定された回数だけタブ名の生成を試行して失敗した場合</exception>
579         public string MakeTabName(string baseTabName, int retryCount)
580         {
581             if (!this.ContainsTab(baseTabName))
582                 return baseTabName;
583
584             foreach (var i in Enumerable.Range(2, retryCount - 1))
585             {
586                 var tabName = baseTabName + i;
587                 if (!this.ContainsTab(tabName))
588                 {
589                     return tabName;
590                 }
591             }
592
593             var message = string.Format(Properties.Resources.TabNameDuplicate_Text, baseTabName);
594             throw new TabException(message);
595         }
596
597         public Dictionary<string, TabClass> Tabs
598         {
599             get
600             {
601                 return _tabs;
602             }
603             set
604             {
605                 _tabs = value;
606             }
607         }
608
609         public Dictionary<string, TabClass>.KeyCollection KeysTab
610         {
611             get
612             {
613                 return _tabs.Keys;
614             }
615         }
616
617         public SortOrder SortOrder { get; private set; }
618
619         public ComparerMode SortMode { get; private set; }
620
621         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
622         {
623             this.SortMode = mode;
624             this.SortOrder = sortOrder;
625
626             foreach (var tab in this._tabs.Values)
627                 tab.SetSortMode(mode, sortOrder);
628         }
629
630         public SortOrder ToggleSortOrder(ComparerMode sortMode)
631         {
632             var sortOrder = this.SortOrder;
633
634             if (this.SortMode == sortMode)
635             {
636                 if (sortOrder == SortOrder.Ascending)
637                     sortOrder = SortOrder.Descending;
638                 else
639                     sortOrder = SortOrder.Ascending;
640             }
641             else
642             {
643                 sortOrder = SortOrder.Ascending;
644             }
645
646             this.SetSortMode(sortMode, sortOrder);
647
648             return this.SortOrder;
649         }
650
651     //    public PostClass RetweetSource(long Id)
652     //    {
653     //        get
654     //        {
655     //            if (_retweets.ContainsKey(Id))
656     //            {
657     //                return _retweets[Id];
658     //            }
659     //            else
660     //            {
661     //                return null;
662     //            }
663     //        }
664     //    }
665         public PostClass RetweetSource(long Id)
666         {
667             PostClass status;
668             return _retweets.TryGetValue(Id, out status)
669                 ? status
670                 : null;
671         }
672
673         public void RemoveFavPost(long Id)
674         {
675             lock (LockObj)
676             {
677                 PostClass post;
678                 var tab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
679
680                 if (_statuses.TryGetValue(Id, out post))
681                 {
682                     //指定タブから該当ID削除
683                     var tType = tab.TabType;
684                     if (tab.Contains(Id))
685                         tab.Remove(Id);
686
687                     //FavタブからRetweet発言を削除する場合は、他の同一参照Retweetも削除
688                     if (tType == MyCommon.TabUsageType.Favorites && post.RetweetedId != null)
689                     {
690                         for (int i = 0; i < tab.AllCount; i++)
691                         {
692                             PostClass rPost = null;
693                             try
694                             {
695                                 rPost = tab[i];
696                             }
697                             catch (ArgumentOutOfRangeException)
698                             {
699                                 break;
700                             }
701                             if (rPost.RetweetedId != null && rPost.RetweetedId == post.RetweetedId)
702                             {
703                                 tab.Remove(rPost.StatusId);
704                             }
705                         }
706                     }
707                 }
708                 //TabType=PublicSearchの場合(Postの保存先がTabClass内)
709                 //if (tab.Contains(StatusId) &&
710                 //   (tab.TabType = MyCommon.TabUsageType.PublicSearch || tab.TabType = MyCommon.TabUsageType.DirectMessage))
711                 //{
712                 //    tab.Remove(StatusId);
713                 //}
714             }
715         }
716
717         public void ScrubGeoReserve(long id, long upToStatusId)
718         {
719             lock (LockObj)
720             {
721                 //this._scrubGeo.Add(new ScrubGeoInfo With {.UserId = id, .UpToStatusId = upToStatusId});
722                 this.ScrubGeo(id, upToStatusId);
723             }
724         }
725
726         private void ScrubGeo(long userId, long upToStatusId)
727         {
728             lock (LockObj)
729             {
730                 var userPosts = from post in this._statuses.Values
731                                 where post.UserId == userId && post.UserId <= upToStatusId
732                                 select post;
733
734                 foreach (var p in userPosts)
735                 {
736                     p.PostGeo = null;
737                 }
738
739                 var userPosts2 = from tb in this.GetTabsInnerStorageType()
740                                  from post in tb.Posts.Values
741                                  where post.UserId == userId && post.UserId <= upToStatusId
742                                  select post;
743
744                 foreach (var p in userPosts2)
745                 {
746                     p.PostGeo = null;
747                 }
748             }
749         }
750
751         public void RemovePostReserve(long id)
752         {
753             lock (LockObj)
754             {
755                 this.deleteQueue.Enqueue(id);
756                 this.DeletePost(id);   //UI選択行がずれるため、RemovePostは使用しない
757             }
758         }
759
760         public void RemovePost(long Id)
761         {
762             lock (LockObj)
763             {
764                 //各タブから該当ID削除
765                 foreach (var tab in _tabs.Values)
766                 {
767                     if (tab.Contains(Id))
768                         tab.Remove(Id);
769                 }
770
771                 PostClass removedPost;
772                 _statuses.TryRemove(Id, out removedPost);
773             }
774         }
775
776         private void DeletePost(long Id)
777         {
778             lock (LockObj)
779             {
780                 PostClass post;
781                 if (_statuses.TryGetValue(Id, out post))
782                 {
783                     post.IsDeleted = true;
784                 }
785                 foreach (var tb in this.GetTabsInnerStorageType())
786                 {
787                     if (tb.Contains(Id))
788                     {
789                         post = tb.Posts[Id];
790                         post.IsDeleted = true;
791                     }
792                 }
793             }
794         }
795
796         public int SubmitUpdate(bool isUserStream = false)
797         {
798             string soundFile;
799             PostClass[] notifyPosts;
800             bool isMentionIncluded, isDeletePost;
801
802             return this.SubmitUpdate(out soundFile, out notifyPosts, out isMentionIncluded,
803                 out isDeletePost, isUserStream);
804         }
805
806         public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
807             out bool isMentionIncluded, out bool isDeletePost, bool isUserStream)
808         {
809             // 注:メインスレッドから呼ぶこと
810             lock (this.LockObj)
811             {
812                 soundFile = "";
813                 notifyPosts = new PostClass[0];
814                 isMentionIncluded = false;
815                 isDeletePost = false;
816
817                 var totalPosts = 0;
818                 var notifyPostsList = new List<PostClass>();
819
820                 var currentNotifyPriority = -1;
821
822                 foreach (var tab in this._tabs.Values)
823                 {
824                     // 振分確定 (各タブに反映)
825                     var addedIds = tab.AddSubmit();
826
827                     if (tab.TabType == MyCommon.TabUsageType.Mentions)
828                     {
829                         if (addedIds.Select(x => tab.Posts[x]).Any(x => x.IsReply))
830                             isMentionIncluded = true;
831                     }
832
833                     if (addedIds.Count != 0 && tab.Notify)
834                     {
835                         // 通知対象のリストに追加
836                         foreach (var statusId in addedIds)
837                         {
838                             PostClass post;
839                             if (tab.Posts.TryGetValue(statusId, out post))
840                                 notifyPostsList.Add(post);
841                         }
842
843                         int notifyPriority;
844                         if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out notifyPriority))
845                             notifyPriority = 0;
846
847                         if (notifyPriority > currentNotifyPriority)
848                         {
849                             // より優先度の高い通知を再生する
850                             soundFile = tab.SoundFile;
851                             currentNotifyPriority = notifyPriority;
852                         }
853                     }
854
855                     totalPosts += addedIds.Count;
856                 }
857
858                 notifyPosts = notifyPostsList.Distinct().ToArray();
859
860                 if (isUserStream)
861                 {
862                     long statusId;
863                     while (this.deleteQueue.TryDequeue(out statusId))
864                     {
865                         this.RemovePost(statusId);
866                         isDeletePost = true;
867                     }
868                 }
869
870                 return totalPosts;
871             }
872         }
873
874         public int DistributePosts()
875         {
876             lock (this.LockObj)
877             {
878                 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
879                 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
880                 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
881
882                 var distributableTabs = this._tabs.Values.Where(x => x.IsDistributableTabType)
883                     .ToArray();
884
885                 var adddedCount = 0;
886
887                 long statusId;
888                 while (this.addQueue.TryDequeue(out statusId))
889                 {
890                     PostClass post;
891                     if (!this._statuses.TryGetValue(statusId, out post))
892                         continue;
893
894                     var filterHit = false; // フィルタにヒットしたタブがあるか
895                     var mark = false; // フィルタによってマーク付けされたか
896                     var excludedReply = false; // リプライから除外されたか
897                     var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
898
899                     foreach (var tab in distributableTabs)
900                     {
901                         // 各振り分けタブのフィルタを実行する
902                         switch (tab.AddFiltered(post))
903                         {
904                             case MyCommon.HITRESULT.Copy:
905                                 filterHit = true;
906                                 break;
907                             case MyCommon.HITRESULT.CopyAndMark:
908                                 filterHit = true;
909                                 mark = true;
910                                 break;
911                             case MyCommon.HITRESULT.Move:
912                                 filterHit = true;
913                                 moved = true;
914                                 break;
915                             case MyCommon.HITRESULT.None:
916                                 break;
917                             case MyCommon.HITRESULT.Exclude:
918                                 if (tab.TabType == MyCommon.TabUsageType.Mentions)
919                                     excludedReply = true;
920                                 break;
921                         }
922                     }
923
924                     post.FilterHit = filterHit;
925                     post.IsMark = mark;
926                     post.IsExcludeReply = excludedReply;
927
928                     // 移動されなかったらRecentに追加
929                     if (!moved)
930                         homeTab.AddPostQueue(post.StatusId, post.IsRead);
931
932                     // 除外ルール適用のないReplyならReplyタブに追加
933                     if (post.IsReply && !excludedReply)
934                         replyTab.AddPostQueue(post.StatusId, post.IsRead);
935
936                     // Fav済み発言だったらFavoritesタブに追加
937                     if (post.IsFav)
938                         favTab.AddPostQueue(post.StatusId, post.IsRead);
939
940                     adddedCount++;
941                 }
942
943                 return adddedCount;
944             }
945         }
946
947         public void AddPost(PostClass Item)
948         {
949             Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
950
951             lock (LockObj)
952             {
953                 if (this.IsMuted(Item))
954                     return;
955
956                 PostClass status;
957                 if (_statuses.TryGetValue(Item.StatusId, out status))
958                 {
959                     if (Item.IsFav)
960                     {
961                         if (Item.RetweetedId == null)
962                         {
963                             status.IsFav = true;
964                         }
965                         else
966                         {
967                             Item.IsFav = false;
968                         }
969                     }
970                     else
971                     {
972                         return;        //追加済みなら何もしない
973                     }
974                 }
975                 else
976                 {
977                     if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
978                     //既に持っている公式RTは捨てる
979                     if (SettingCommon.Instance.HideDuplicatedRetweets &&
980                         !Item.IsMe &&
981                         Item.RetweetedId != null &&
982                         this._retweets.TryGetValue(Item.RetweetedId.Value, out status) &&
983                         status.RetweetedCount > 0) return;
984
985                     if (BlockIds.Contains(Item.UserId))
986                         return;
987
988                     _statuses.TryAdd(Item.StatusId, Item);
989                 }
990                 if (Item.RetweetedId != null)
991                 {
992                     this.AddRetweet(Item);
993                 }
994                 if (Item.IsFav && _retweets.ContainsKey(Item.StatusId))
995                 {
996                     return;    //Fav済みのRetweet元発言は追加しない
997                 }
998                 this.addQueue.Enqueue(Item.StatusId);
999             }
1000         }
1001
1002         public bool IsMuted(PostClass post)
1003         {
1004             var muteTab = this.GetTabByType(MyCommon.TabUsageType.Mute);
1005             if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
1006                 return true;
1007
1008             // これ以降は Twitter 標準のミュート機能に準じた判定
1009
1010             // リプライはミュート対象外
1011             // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
1012             if (post.IsReply)
1013                 return false;
1014
1015             if (this.MuteUserIds.Contains(post.UserId))
1016                 return true;
1017
1018             if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
1019                 return true;
1020
1021             return false;
1022         }
1023
1024         private void AddRetweet(PostClass item)
1025         {
1026             var retweetedId = item.RetweetedId.Value;
1027
1028             PostClass status;
1029             if (this._retweets.TryGetValue(retweetedId, out status))
1030             {
1031                 status.RetweetedCount++;
1032                 if (status.RetweetedCount > 10)
1033                 {
1034                     status.RetweetedCount = 0;
1035                 }
1036                 return;
1037             }
1038
1039             this._retweets.Add(retweetedId, item.ConvertToOriginalPost());
1040         }
1041
1042         public bool AddQuoteTweet(PostClass item)
1043         {
1044             lock (LockObj)
1045             {
1046                 if (IsMuted(item) || BlockIds.Contains(item.UserId))
1047                     return false;
1048
1049                 _quotes[item.StatusId] = item;
1050                 return true;
1051             }
1052         }
1053
1054         /// <summary>
1055         /// 全てのタブを横断して既読状態を変更します
1056         /// </summary>
1057         /// <param name="statusId">変更するツイートのID</param>
1058         /// <param name="read">既読状態</param>
1059         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1060         public bool SetReadAllTab(long statusId, bool read)
1061         {
1062             lock (LockObj)
1063             {
1064                 foreach (var tab in this._tabs.Values)
1065                 {
1066                     if (!tab.Contains(statusId))
1067                         continue;
1068
1069                     tab.SetReadState(statusId, read);
1070                 }
1071
1072                 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
1073                 PostClass post;
1074                 if (this.Posts.TryGetValue(statusId, out post))
1075                     post.IsRead = read;
1076
1077                 return true;
1078             }
1079         }
1080
1081         /// <summary>
1082         /// Home タブのツイートを全て既読にします。
1083         /// ただし IsReply または FilterHit が true なものを除きます。
1084         /// </summary>
1085         public void SetReadHomeTab()
1086         {
1087             var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
1088
1089             lock (LockObj)
1090             {
1091                 foreach (var statusId in homeTab.GetUnreadIds())
1092                 {
1093                     PostClass post;
1094                     if (!this.Posts.TryGetValue(statusId, out post))
1095                         continue;
1096
1097                     if (post.IsReply || post.FilterHit)
1098                         continue;
1099
1100                     this.SetReadAllTab(post.StatusId, read: true);
1101                 }
1102             }
1103         }
1104
1105         public PostClass this[long ID]
1106         {
1107             get
1108             {
1109                 PostClass status;
1110                 if (this._statuses.TryGetValue(ID, out status))
1111                     return status;
1112
1113                 if (this._retweets.TryGetValue(ID, out status))
1114                     return status;
1115
1116                 if (this._quotes.TryGetValue(ID, out status))
1117                     return status;
1118
1119                 return this.GetTabsInnerStorageType()
1120                     .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
1121                     .Where(x => x != null)
1122                     .FirstOrDefault();
1123             }
1124         }
1125
1126         public bool ContainsKey(long Id)
1127         {
1128             //DM,公式検索は非対応
1129             lock (LockObj)
1130             {
1131                 return _statuses.ContainsKey(Id);
1132             }
1133         }
1134
1135         public void RenameTab(string Original, string NewName)
1136         {
1137             var tb = _tabs[Original];
1138             _tabs.Remove(Original);
1139             tb.TabName = NewName;
1140             _tabs.Add(NewName, tb);
1141         }
1142
1143         public void FilterAll()
1144         {
1145             lock (LockObj)
1146             {
1147                 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
1148                 var detachedIdsAll = Enumerable.Empty<long>();
1149
1150                 foreach (var tab in _tabs.Values.ToArray())
1151                 {
1152                     if (!tab.IsDistributableTabType)
1153                         continue;
1154
1155                     if (tab.TabType == MyCommon.TabUsageType.Mute)
1156                         continue;
1157
1158                     // フィルタに変更のあったタブのみを対象とする
1159                     if (!tab.FilterModified)
1160                         continue;
1161
1162                     tab.FilterModified = false;
1163
1164                     // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
1165                     var orgIds = tab.BackupIds;
1166                     tab.ClearIDs();
1167
1168                     foreach (var post in _statuses.Values)
1169                     {
1170                         var filterHit = false; // フィルタにヒットしたタブがあるか
1171                         var mark = false; // フィルタによってマーク付けされたか
1172                         var excluded = false; // 除外フィルタによって除外されたか
1173                         var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
1174
1175                         switch (tab.AddFiltered(post, immediately: true))
1176                         {
1177                             case MyCommon.HITRESULT.Copy:
1178                                 filterHit = true;
1179                                 break;
1180                             case MyCommon.HITRESULT.CopyAndMark:
1181                                 filterHit = true;
1182                                 mark = true;
1183                                 break;
1184                             case MyCommon.HITRESULT.Move:
1185                                 filterHit = true;
1186                                 moved = true;
1187                                 break;
1188                             case MyCommon.HITRESULT.None:
1189                                 break;
1190                             case MyCommon.HITRESULT.Exclude:
1191                                 excluded = true;
1192                                 break;
1193                         }
1194
1195                         post.FilterHit = filterHit;
1196                         post.IsMark = mark;
1197
1198                         // 移動されたらRecentから除去
1199                         if (moved)
1200                             homeTab.Remove(post.StatusId);
1201
1202                         if (tab.TabType == MyCommon.TabUsageType.Mentions)
1203                         {
1204                             post.IsExcludeReply = excluded;
1205
1206                             // 除外ルール適用のないReplyならReplyタブに追加
1207                             if (post.IsReply && !excluded)
1208                                 tab.AddPostImmediately(post.StatusId, post.IsRead);
1209                         }
1210                     }
1211
1212                     // フィルタの更新によってタブから取り除かれたツイートのID
1213                     var detachedIds = orgIds.Except(tab.BackupIds).ToArray();
1214
1215                     detachedIdsAll = detachedIdsAll.Concat(detachedIds);
1216                 }
1217
1218                 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
1219                 foreach (var id in detachedIdsAll)
1220                 {
1221                     var hit = false;
1222                     foreach (var tbTemp in _tabs.Values.ToArray())
1223                     {
1224                         if (!tbTemp.IsDistributableTabType)
1225                             continue;
1226
1227                         if (tbTemp.Contains(id))
1228                         {
1229                             hit = true;
1230                             break;
1231                         }
1232                     }
1233
1234                     if (!hit)
1235                     {
1236                         PostClass post;
1237                         if (this._statuses.TryGetValue(id, out post))
1238                             homeTab.AddPostImmediately(post.StatusId, post.IsRead);
1239                     }
1240                 }
1241             }
1242         }
1243
1244         public void ClearTabIds(string TabName)
1245         {
1246             //不要なPostを削除
1247             lock (LockObj)
1248             {
1249                 var tb = _tabs[TabName];
1250                 if (!tb.IsInnerStorageTabType)
1251                 {
1252                     foreach (var Id in tb.BackupIds)
1253                     {
1254                         var Hit = false;
1255                         foreach (var tab in _tabs.Values)
1256                         {
1257                             if (tab.Contains(Id))
1258                             {
1259                                 Hit = true;
1260                                 break;
1261                             }
1262                         }
1263                         if (!Hit)
1264                         {
1265                             PostClass removedPost;
1266                             _statuses.TryRemove(Id, out removedPost);
1267                         }
1268                     }
1269                 }
1270
1271                 //指定タブをクリア
1272                 tb.ClearIDs();
1273             }
1274         }
1275
1276         public void RefreshOwl(ISet<long> follower)
1277         {
1278             lock (LockObj)
1279             {
1280                 if (follower.Count > 0)
1281                 {
1282                     foreach (var post in _statuses.Values)
1283                     {
1284                         //if (post.UserId = 0 || post.IsDm) Continue For
1285                         if (post.IsMe)
1286                         {
1287                             post.IsOwl = false;
1288                         }
1289                         else
1290                         {
1291                             post.IsOwl = !follower.Contains(post.UserId);
1292                         }
1293                     }
1294                 }
1295                 else
1296                 {
1297                     foreach (var post in _statuses.Values)
1298                     {
1299                         post.IsOwl = false;
1300                     }
1301                 }
1302             }
1303         }
1304
1305         public TabClass GetTabByType(MyCommon.TabUsageType tabType)
1306         {
1307             //Home,Mentions,DM,Favは1つに制限する
1308             //その他のタイプを指定されたら、最初に合致したものを返す
1309             //合致しなければnullを返す
1310             lock (LockObj)
1311             {
1312                 return this._tabs.Values
1313                     .FirstOrDefault(x => x.TabType.HasFlag(tabType));
1314             }
1315         }
1316
1317         public TabClass[] GetTabsByType(MyCommon.TabUsageType tabType)
1318         {
1319             lock (LockObj)
1320             {
1321                 return this._tabs.Values
1322                     .Where(x => x.TabType.HasFlag(tabType))
1323                     .ToArray();
1324             }
1325         }
1326
1327         public TabClass[] GetTabsInnerStorageType()
1328         {
1329             lock (LockObj)
1330             {
1331                 return this._tabs.Values
1332                     .Where(x => x.IsInnerStorageTabType)
1333                     .ToArray();
1334             }
1335         }
1336
1337         public TabClass GetTabByName(string tabName)
1338         {
1339             lock (LockObj)
1340             {
1341                 TabClass tab;
1342                 return _tabs.TryGetValue(tabName, out tab)
1343                     ? tab
1344                     : null;
1345             }
1346         }
1347
1348         public ConcurrentDictionary<long, PostClass> Posts
1349             => this._statuses;
1350     }
1351
1352     [Serializable]
1353     public sealed class TabClass
1354     {
1355         private List<PostFilterRule> _filters;
1356         private IndexedSortedSet<long> _ids;
1357         private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
1358         private SortedSet<long> unreadIds = new SortedSet<long>();
1359         private MyCommon.TabUsageType _tabType = MyCommon.TabUsageType.Undefined;
1360
1361         private readonly object _lockObj = new object();
1362
1363         public string User { get; set; }
1364
1365 #region "検索"
1366         //Search query
1367         private string _searchLang = "";
1368         private string _searchWords = "";
1369
1370         public string SearchLang
1371         {
1372             get
1373             {
1374                 return _searchLang;
1375             }
1376             set
1377             {
1378                 _searchLang = value;
1379                 this.ResetFetchIds();
1380             }
1381         }
1382         public string SearchWords
1383         {
1384             get
1385             {
1386                 return _searchWords;
1387             }
1388             set
1389             {
1390                 _searchWords = value.Trim();
1391                 this.ResetFetchIds();
1392             }
1393         }
1394
1395         private Dictionary<string, string> _beforeQuery = new Dictionary<string, string>();
1396
1397         public bool IsSearchQueryChanged
1398         {
1399             get
1400             {
1401                 var qry = new Dictionary<string, string>();
1402                 if (!string.IsNullOrEmpty(_searchWords))
1403                 {
1404                     qry.Add("q", _searchWords);
1405                     if (!string.IsNullOrEmpty(_searchLang)) qry.Add("lang", _searchLang);
1406                 }
1407                 if (qry.Count != _beforeQuery.Count)
1408                 {
1409                     _beforeQuery = qry;
1410                     return true;
1411                 }
1412
1413                 foreach (var kvp in qry)
1414                 {
1415                     string value;
1416                     if (!_beforeQuery.TryGetValue(kvp.Key, out value) || value != kvp.Value)
1417                     {
1418                         _beforeQuery = qry;
1419                         return true;
1420                     }
1421                 }
1422                 return false;
1423             }
1424         }
1425 #endregion
1426
1427 #region "リスト"
1428         [NonSerialized]
1429         private ListElement _listInfo;
1430         public ListElement ListInfo
1431         {
1432             get
1433             {
1434                 return _listInfo;
1435             }
1436             set
1437             {
1438                 _listInfo = value;
1439             }
1440         }
1441 #endregion
1442
1443         [XmlIgnore]
1444         public PostClass RelationTargetPost { get; set; }
1445
1446         [XmlIgnore]
1447         public long OldestId = long.MaxValue;
1448
1449         [XmlIgnore]
1450         public long SinceId { get; set; }
1451
1452         [XmlIgnore]
1453         public ConcurrentDictionary<long, PostClass> Posts { get; private set; }
1454
1455         private ConcurrentDictionary<long, PostClass> _innerPosts;
1456
1457         private struct TemporaryId
1458         {
1459             public long Id;
1460             public bool Read;
1461
1462             public TemporaryId(long argId, bool argRead)
1463             {
1464                 Id = argId;
1465                 Read = argRead;
1466             }
1467         }
1468
1469         public TabClass()
1470         {
1471             _innerPosts = new ConcurrentDictionary<long, PostClass>();
1472             Posts = _innerPosts;
1473             SoundFile = "";
1474             TabName = "";
1475             _filters = new List<PostFilterRule>();
1476             Protected = false;
1477             Notify = true;
1478             SoundFile = "";
1479             UnreadManage = true;
1480             _ids = new IndexedSortedSet<long>();
1481             _tabType = MyCommon.TabUsageType.Undefined;
1482             _listInfo = null;
1483         }
1484
1485         public TabClass(string TabName, MyCommon.TabUsageType TabType, ListElement list) : this()
1486         {
1487             this.TabName = TabName;
1488             this.TabType = TabType;
1489             this.ListInfo = list;
1490         }
1491
1492         /// <summary>
1493         /// タブ更新時に使用する SinceId, OldestId をリセットする
1494         /// </summary>
1495         public void ResetFetchIds()
1496         {
1497             this.SinceId = 0L;
1498             this.OldestId = long.MaxValue;
1499         }
1500
1501         /// <summary>
1502         /// ソート対象のフィールドとソート順を設定し、ソートを実行します
1503         /// </summary>
1504         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
1505         {
1506             this.SortMode = mode;
1507             this.SortOrder = sortOrder;
1508
1509             this.ApplySortMode();
1510         }
1511
1512         private void ApplySortMode()
1513         {
1514             var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
1515
1516             Comparison<long> comparison;
1517             if (this.SortMode == ComparerMode.Id)
1518             {
1519                 comparison = (x, y) => sign * x.CompareTo(y);
1520             }
1521             else
1522             {
1523                 Comparison<PostClass> postComparison;
1524                 switch (this.SortMode)
1525                 {
1526                     case ComparerMode.Data:
1527                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
1528                         break;
1529                     case ComparerMode.Name:
1530                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
1531                         break;
1532                     case ComparerMode.Nickname:
1533                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
1534                         break;
1535                     case ComparerMode.Source:
1536                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
1537                         break;
1538                     default:
1539                         throw new InvalidEnumArgumentException();
1540                 }
1541
1542                 comparison = (x, y) =>
1543                 {
1544                     PostClass xPost, yPost;
1545                     this.Posts.TryGetValue(x, out xPost);
1546                     this.Posts.TryGetValue(y, out yPost);
1547
1548                     var compare = sign * postComparison(xPost, yPost);
1549                     if (compare != 0)
1550                         return compare;
1551
1552                     // 同値であれば status_id で比較する
1553                     return sign * x.CompareTo(y);
1554                 };
1555             }
1556
1557             var comparer = Comparer<long>.Create(comparison);
1558
1559             this._ids = new IndexedSortedSet<long>(this._ids, comparer);
1560             this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
1561         }
1562
1563         [XmlIgnore]
1564         public ComparerMode SortMode { get; private set; }
1565
1566         [XmlIgnore]
1567         public SortOrder SortOrder { get; private set; }
1568
1569         public void AddPostQueue(long statusId, bool read)
1570         {
1571             this.addQueue.Enqueue(new TemporaryId(statusId, read));
1572         }
1573
1574         //無条件に追加
1575         public void AddPostImmediately(long ID, bool Read)
1576         {
1577             if (this._ids.Contains(ID)) return;
1578
1579             this._ids.Add(ID);
1580
1581             if (!Read)
1582                 this.unreadIds.Add(ID);
1583         }
1584
1585         //フィルタに合致したら追加
1586         public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false)
1587         {
1588             if (this.IsInnerStorageTabType) return MyCommon.HITRESULT.None;
1589
1590             var rslt = MyCommon.HITRESULT.None;
1591             //全フィルタ評価(優先順位あり)
1592             lock (this._lockObj)
1593             {
1594                 foreach (var ft in _filters)
1595                 {
1596                     try
1597                     {
1598                         switch (ft.ExecFilter(post))   //フィルタクラスでヒット判定
1599                         {
1600                             case MyCommon.HITRESULT.None:
1601                                 break;
1602                             case MyCommon.HITRESULT.Copy:
1603                                 if (rslt != MyCommon.HITRESULT.CopyAndMark) rslt = MyCommon.HITRESULT.Copy;
1604                                 break;
1605                             case MyCommon.HITRESULT.CopyAndMark:
1606                                 rslt = MyCommon.HITRESULT.CopyAndMark;
1607                                 break;
1608                             case MyCommon.HITRESULT.Move:
1609                                 rslt = MyCommon.HITRESULT.Move;
1610                                 break;
1611                             case MyCommon.HITRESULT.Exclude:
1612                                 rslt = MyCommon.HITRESULT.Exclude;
1613                                 goto exit_for;
1614                         }
1615                     }
1616                     catch (NullReferenceException)
1617                     {
1618                         // ExecFilterでNullRef出る場合あり。暫定対応
1619                         MyCommon.TraceOut("ExecFilterでNullRef: " + ft.ToString());
1620                         rslt = MyCommon.HITRESULT.None;
1621                     }
1622                 }
1623             exit_for:
1624                 ;
1625             }
1626
1627             if (this.TabType != MyCommon.TabUsageType.Mute &&
1628                 rslt != MyCommon.HITRESULT.None && rslt != MyCommon.HITRESULT.Exclude)
1629             {
1630                 if (immediately)
1631                     this.AddPostImmediately(post.StatusId, post.IsRead);
1632                 else
1633                     this.AddPostQueue(post.StatusId, post.IsRead);
1634             }
1635
1636             return rslt; //マーク付けは呼び出し元で行うこと
1637         }
1638
1639         //検索結果の追加
1640         public void AddPostToInnerStorage(PostClass Post)
1641         {
1642             if (_innerPosts.ContainsKey(Post.StatusId)) return;
1643             _innerPosts.TryAdd(Post.StatusId, Post);
1644             this.AddPostQueue(Post.StatusId, Post.IsRead);
1645         }
1646
1647         public IList<long> AddSubmit()
1648         {
1649             var addedIds = new List<long>();
1650
1651             TemporaryId tId;
1652             while (this.addQueue.TryDequeue(out tId))
1653             {
1654                 this.AddPostImmediately(tId.Id, tId.Read);
1655                 addedIds.Add(tId.Id);
1656             }
1657
1658             return addedIds;
1659         }
1660
1661         public void Remove(long Id)
1662         {
1663             if (!this._ids.Contains(Id))
1664                 return;
1665
1666             this._ids.Remove(Id);
1667             this.unreadIds.Remove(Id);
1668
1669             if (this.IsInnerStorageTabType)
1670             {
1671                 PostClass removedPost;
1672                 this._innerPosts.TryRemove(Id, out removedPost);
1673             }
1674         }
1675
1676         public bool UnreadManage { get; set; }
1677
1678         // v1.0.5で「タブを固定(Locked)」から「タブを保護(Protected)」に名称変更
1679         [XmlElement(ElementName = "Locked")]
1680         public bool Protected { get; set; }
1681
1682         public bool Notify { get; set; }
1683
1684         public string SoundFile { get; set; }
1685
1686         /// <summary>
1687         /// 次に表示する未読ツイートのIDを返します。
1688         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1689         /// </summary>
1690         [XmlIgnore]
1691         public long NextUnreadId
1692         {
1693             get
1694             {
1695                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1696                     return -1L;
1697
1698                 if (this.unreadIds.Count == 0)
1699                     return -1L;
1700
1701                 // unreadIds はリストのインデックス番号順に並んでいるため、
1702                 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
1703                 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
1704             }
1705         }
1706
1707         /// <summary>
1708         /// 次に表示する未読ツイートのインデックス番号を返します。
1709         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1710         /// </summary>
1711         [XmlIgnore]
1712         public int NextUnreadIndex
1713         {
1714             get
1715             {
1716                 var unreadId = this.NextUnreadId;
1717                 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
1718             }
1719         }
1720
1721         /// <summary>
1722         /// 未読ツイートの件数を返します。
1723         /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
1724         /// </summary>
1725         [XmlIgnore]
1726         public int UnreadCount
1727         {
1728             get
1729             {
1730                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1731                     return 0;
1732
1733                 return this.unreadIds.Count;
1734             }
1735         }
1736
1737         public int AllCount
1738         {
1739             get
1740             {
1741                 return this._ids.Count;
1742             }
1743         }
1744
1745         /// <summary>
1746         /// 未読ツイートの ID を配列で返します
1747         /// </summary>
1748         public long[] GetUnreadIds()
1749         {
1750             lock (this._lockObj)
1751             {
1752                 return this.unreadIds.ToArray();
1753             }
1754         }
1755
1756         /// <summary>
1757         /// タブ内の既読状態を変更します
1758         /// </summary>
1759         /// <remarks>
1760         /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
1761         /// </remarks>
1762         /// <param name="statusId">変更するツイートのID</param>
1763         /// <param name="read">既読状態</param>
1764         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1765         internal bool SetReadState(long statusId, bool read)
1766         {
1767             if (!this._ids.Contains(statusId))
1768                 throw new ArgumentException(nameof(statusId));
1769
1770             if (this.IsInnerStorageTabType)
1771                 this.Posts[statusId].IsRead = read;
1772
1773             if (read)
1774                 return this.unreadIds.Remove(statusId);
1775             else
1776                 return this.unreadIds.Add(statusId);
1777         }
1778
1779         public PostFilterRule[] GetFilters()
1780         {
1781             lock (this._lockObj)
1782             {
1783                 return _filters.ToArray();
1784             }
1785         }
1786
1787         public void RemoveFilter(PostFilterRule filter)
1788         {
1789             lock (this._lockObj)
1790             {
1791                 _filters.Remove(filter);
1792                 filter.PropertyChanged -= this.OnFilterModified;
1793                 this.FilterModified = true;
1794             }
1795         }
1796
1797         public bool AddFilter(PostFilterRule filter)
1798         {
1799             lock (this._lockObj)
1800             {
1801                 if (_filters.Contains(filter)) return false;
1802                 filter.PropertyChanged += this.OnFilterModified;
1803                 _filters.Add(filter);
1804                 this.FilterModified = true;
1805                 return true;
1806             }
1807         }
1808
1809         private void OnFilterModified(object sender, PropertyChangedEventArgs e)
1810         {
1811             this.FilterModified = true;
1812         }
1813
1814         public PostFilterRule[] FilterArray
1815         {
1816             get
1817             {
1818                 lock (this._lockObj)
1819                 {
1820                     return _filters.ToArray();
1821                 }
1822             }
1823             set
1824             {
1825                 lock (this._lockObj)
1826                 {
1827                     foreach (var oldFilter in this._filters)
1828                     {
1829                         oldFilter.PropertyChanged -= this.OnFilterModified;
1830                     }
1831
1832                     this._filters.Clear();
1833                     this.FilterModified = true;
1834
1835                     foreach (var newFilter in value)
1836                     {
1837                         _filters.Add(newFilter);
1838                         newFilter.PropertyChanged += this.OnFilterModified;
1839                     }
1840                 }
1841             }
1842         }
1843         public bool Contains(long ID)
1844         {
1845             return _ids.Contains(ID);
1846         }
1847
1848         public void ClearIDs()
1849         {
1850             _ids.Clear();
1851             this.unreadIds.Clear();
1852             _innerPosts.Clear();
1853
1854             Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
1855         }
1856
1857         public PostClass this[int Index]
1858         {
1859             get
1860             {
1861                 var id = GetId(Index);
1862                 if (id < 0) throw new ArgumentException("Index can't find. Index=" + Index.ToString() + "/TabName=" + TabName, nameof(Index));
1863                 return Posts[id];
1864             }
1865         }
1866
1867         public PostClass[] this[int StartIndex, int EndIndex]
1868         {
1869             get
1870             {
1871                 var length = EndIndex - StartIndex + 1;
1872                 var posts = new PostClass[length];
1873                 for (int i = 0; i < length; i++)
1874                 {
1875                     posts[i] = Posts[GetId(StartIndex + i)];
1876                 }
1877                 return posts;
1878             }
1879         }
1880
1881         public long[] GetId(ListView.SelectedIndexCollection IndexCollection)
1882         {
1883             if (IndexCollection.Count == 0) return null;
1884
1885             var Ids = new long[IndexCollection.Count];
1886             for (int i = 0; i < Ids.Length; i++)
1887             {
1888                 Ids[i] = GetId(IndexCollection[i]);
1889             }
1890             return Ids;
1891         }
1892
1893         public long GetId(int Index)
1894         {
1895             return Index < _ids.Count ? _ids[Index] : -1;
1896         }
1897
1898         public int[] IndexOf(long[] Ids)
1899         {
1900             if (Ids == null) return null;
1901             var idx = new int[Ids.Length];
1902             for (int i = 0; i < Ids.Length; i++)
1903             {
1904                 idx[i] = IndexOf(Ids[i]);
1905             }
1906             return idx;
1907         }
1908
1909         public int IndexOf(long ID)
1910         {
1911             return _ids.IndexOf(ID);
1912         }
1913
1914         [XmlIgnore]
1915         public bool FilterModified { get; set; }
1916
1917         public long[] BackupIds
1918         {
1919             get
1920             {
1921                 return _ids.ToArray();
1922             }
1923         }
1924
1925         public string TabName { get; set; }
1926
1927         public MyCommon.TabUsageType TabType
1928         {
1929             get
1930             {
1931                 return _tabType;
1932             }
1933             set
1934             {
1935                 _tabType = value;
1936                 if (this.IsInnerStorageTabType)
1937                 {
1938                     Posts = _innerPosts;
1939                 }
1940                 else
1941                 {
1942                     Posts = TabInformations.GetInstance().Posts;
1943                 }
1944             }
1945         }
1946
1947         public bool IsDefaultTabType
1948         {
1949             get
1950             {
1951                 return _tabType.IsDefault();
1952             }
1953         }
1954
1955         public bool IsDistributableTabType
1956         {
1957             get
1958             {
1959                 return _tabType.IsDistributable();
1960             }
1961         }
1962
1963         public bool IsInnerStorageTabType
1964         {
1965             get
1966             {
1967                 return _tabType.IsInnerStorage();
1968             }
1969         }
1970     }
1971
1972     /// <summary>
1973     /// enum TabUsageType に対応する拡張メソッドを定義したクラス
1974     /// </summary>
1975     public static class TabUsageTypeExt
1976     {
1977         const MyCommon.TabUsageType DefaultTabTypeMask =
1978             MyCommon.TabUsageType.Home |
1979             MyCommon.TabUsageType.Mentions |
1980             MyCommon.TabUsageType.DirectMessage |
1981             MyCommon.TabUsageType.Favorites |
1982             MyCommon.TabUsageType.Mute;
1983
1984         const MyCommon.TabUsageType DistributableTabTypeMask =
1985             MyCommon.TabUsageType.Mentions |
1986             MyCommon.TabUsageType.UserDefined |
1987             MyCommon.TabUsageType.Mute;
1988
1989         const MyCommon.TabUsageType InnerStorageTabTypeMask =
1990             MyCommon.TabUsageType.DirectMessage |
1991             MyCommon.TabUsageType.PublicSearch |
1992             MyCommon.TabUsageType.Lists |
1993             MyCommon.TabUsageType.UserTimeline |
1994             MyCommon.TabUsageType.Related |
1995             MyCommon.TabUsageType.SearchResults;
1996
1997         /// <summary>
1998         /// デフォルトタブかどうかを示す値を取得します。
1999         /// </summary>
2000         public static bool IsDefault(this MyCommon.TabUsageType tabType)
2001         {
2002             return (tabType & DefaultTabTypeMask) != 0;
2003         }
2004
2005         /// <summary>
2006         /// 振り分け可能タブかどうかを示す値を取得します。
2007         /// </summary>
2008         public static bool IsDistributable(this MyCommon.TabUsageType tabType)
2009         {
2010             return (tabType & DistributableTabTypeMask) != 0;
2011         }
2012
2013         /// <summary>
2014         /// 内部ストレージを使用するタブかどうかを示す値を取得します。
2015         /// </summary>
2016         public static bool IsInnerStorage(this MyCommon.TabUsageType tabType)
2017         {
2018             return (tabType & InnerStorageTabTypeMask) != 0;
2019         }
2020     }
2021
2022     /// <summary>
2023     /// 比較する方法
2024     /// </summary>
2025     public enum ComparerMode
2026     {
2027         Id,
2028         Data,
2029         Name,
2030         Nickname,
2031         Source,
2032     }
2033 }