OSDN Git Service

TabInformations.SubmitUpdateメソッドのisMentionIncludeをnewMentionOrDmに変更
[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()
797         {
798             string soundFile;
799             PostClass[] notifyPosts;
800             bool newMentionOrDm, isDeletePost;
801
802             return this.SubmitUpdate(out soundFile, out notifyPosts, out newMentionOrDm,
803                 out isDeletePost);
804         }
805
806         public int SubmitUpdate(out string soundFile, out PostClass[] notifyPosts,
807             out bool newMentionOrDm, out bool isDeletePost)
808         {
809             // 注:メインスレッドから呼ぶこと
810             lock (this.LockObj)
811             {
812                 soundFile = "";
813                 notifyPosts = new PostClass[0];
814                 newMentionOrDm = 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                         tab.TabType == MyCommon.TabUsageType.DirectMessage)
829                     {
830                         if (addedIds.Count > 0)
831                             newMentionOrDm = true;
832                     }
833
834                     if (addedIds.Count != 0 && tab.Notify)
835                     {
836                         // 通知対象のリストに追加
837                         foreach (var statusId in addedIds)
838                         {
839                             PostClass post;
840                             if (tab.Posts.TryGetValue(statusId, out post))
841                                 notifyPostsList.Add(post);
842                         }
843
844                         int notifyPriority;
845                         if (!this.notifyPriorityByTabType.TryGetValue(tab.TabType, out notifyPriority))
846                             notifyPriority = 0;
847
848                         if (notifyPriority > currentNotifyPriority)
849                         {
850                             // より優先度の高い通知を再生する
851                             soundFile = tab.SoundFile;
852                             currentNotifyPriority = notifyPriority;
853                         }
854                     }
855
856                     totalPosts += addedIds.Count;
857                 }
858
859                 notifyPosts = notifyPostsList.Distinct().ToArray();
860
861                 long deletedStatusId;
862                 while (this.deleteQueue.TryDequeue(out deletedStatusId))
863                 {
864                     this.RemovePost(deletedStatusId);
865                     isDeletePost = true;
866                 }
867
868                 return totalPosts;
869             }
870         }
871
872         public int DistributePosts()
873         {
874             lock (this.LockObj)
875             {
876                 var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
877                 var replyTab = this.GetTabByType(MyCommon.TabUsageType.Mentions);
878                 var favTab = this.GetTabByType(MyCommon.TabUsageType.Favorites);
879
880                 var distributableTabs = this._tabs.Values.Where(x => x.IsDistributableTabType)
881                     .ToArray();
882
883                 var adddedCount = 0;
884
885                 long statusId;
886                 while (this.addQueue.TryDequeue(out statusId))
887                 {
888                     PostClass post;
889                     if (!this._statuses.TryGetValue(statusId, out post))
890                         continue;
891
892                     var filterHit = false; // フィルタにヒットしたタブがあるか
893                     var mark = false; // フィルタによってマーク付けされたか
894                     var excludedReply = false; // リプライから除外されたか
895                     var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
896
897                     foreach (var tab in distributableTabs)
898                     {
899                         // 各振り分けタブのフィルタを実行する
900                         switch (tab.AddFiltered(post))
901                         {
902                             case MyCommon.HITRESULT.Copy:
903                                 filterHit = true;
904                                 break;
905                             case MyCommon.HITRESULT.CopyAndMark:
906                                 filterHit = true;
907                                 mark = true;
908                                 break;
909                             case MyCommon.HITRESULT.Move:
910                                 filterHit = true;
911                                 moved = true;
912                                 break;
913                             case MyCommon.HITRESULT.None:
914                                 break;
915                             case MyCommon.HITRESULT.Exclude:
916                                 if (tab.TabType == MyCommon.TabUsageType.Mentions)
917                                     excludedReply = true;
918                                 break;
919                         }
920                     }
921
922                     post.FilterHit = filterHit;
923                     post.IsMark = mark;
924                     post.IsExcludeReply = excludedReply;
925
926                     // 移動されなかったらRecentに追加
927                     if (!moved)
928                         homeTab.AddPostQueue(post.StatusId, post.IsRead);
929
930                     // 除外ルール適用のないReplyならReplyタブに追加
931                     if (post.IsReply && !excludedReply)
932                         replyTab.AddPostQueue(post.StatusId, post.IsRead);
933
934                     // Fav済み発言だったらFavoritesタブに追加
935                     if (post.IsFav)
936                         favTab.AddPostQueue(post.StatusId, post.IsRead);
937
938                     adddedCount++;
939                 }
940
941                 return adddedCount;
942             }
943         }
944
945         public void AddPost(PostClass Item)
946         {
947             Debug.Assert(!Item.IsDm, "DM は TabClass.AddPostToInnerStorage を使用する");
948
949             lock (LockObj)
950             {
951                 if (this.IsMuted(Item))
952                     return;
953
954                 PostClass status;
955                 if (_statuses.TryGetValue(Item.StatusId, out status))
956                 {
957                     if (Item.IsFav)
958                     {
959                         if (Item.RetweetedId == null)
960                         {
961                             status.IsFav = true;
962                         }
963                         else
964                         {
965                             Item.IsFav = false;
966                         }
967                     }
968                     else
969                     {
970                         return;        //追加済みなら何もしない
971                     }
972                 }
973                 else
974                 {
975                     if (Item.IsFav && Item.RetweetedId != null) Item.IsFav = false;
976                     //既に持っている公式RTは捨てる
977                     if (SettingCommon.Instance.HideDuplicatedRetweets &&
978                         !Item.IsMe &&
979                         Item.RetweetedId != null &&
980                         this._retweets.TryGetValue(Item.RetweetedId.Value, out status) &&
981                         status.RetweetedCount > 0) return;
982
983                     if (BlockIds.Contains(Item.UserId))
984                         return;
985
986                     _statuses.TryAdd(Item.StatusId, Item);
987                 }
988                 if (Item.RetweetedId != null)
989                 {
990                     this.AddRetweet(Item);
991                 }
992                 if (Item.IsFav && _retweets.ContainsKey(Item.StatusId))
993                 {
994                     return;    //Fav済みのRetweet元発言は追加しない
995                 }
996                 this.addQueue.Enqueue(Item.StatusId);
997             }
998         }
999
1000         public bool IsMuted(PostClass post)
1001         {
1002             var muteTab = this.GetTabByType(MyCommon.TabUsageType.Mute);
1003             if (muteTab != null && muteTab.AddFiltered(post) == MyCommon.HITRESULT.Move)
1004                 return true;
1005
1006             // これ以降は Twitter 標準のミュート機能に準じた判定
1007
1008             // リプライはミュート対象外
1009             // 参照: https://support.twitter.com/articles/20171399-muting-users-on-twitter
1010             if (post.IsReply)
1011                 return false;
1012
1013             if (this.MuteUserIds.Contains(post.UserId))
1014                 return true;
1015
1016             if (post.RetweetedByUserId != null && this.MuteUserIds.Contains(post.RetweetedByUserId.Value))
1017                 return true;
1018
1019             return false;
1020         }
1021
1022         private void AddRetweet(PostClass item)
1023         {
1024             var retweetedId = item.RetweetedId.Value;
1025
1026             PostClass status;
1027             if (this._retweets.TryGetValue(retweetedId, out status))
1028             {
1029                 status.RetweetedCount++;
1030                 if (status.RetweetedCount > 10)
1031                 {
1032                     status.RetweetedCount = 0;
1033                 }
1034                 return;
1035             }
1036
1037             this._retweets.Add(retweetedId, item.ConvertToOriginalPost());
1038         }
1039
1040         public bool AddQuoteTweet(PostClass item)
1041         {
1042             lock (LockObj)
1043             {
1044                 if (IsMuted(item) || BlockIds.Contains(item.UserId))
1045                     return false;
1046
1047                 _quotes[item.StatusId] = item;
1048                 return true;
1049             }
1050         }
1051
1052         /// <summary>
1053         /// 全てのタブを横断して既読状態を変更します
1054         /// </summary>
1055         /// <param name="statusId">変更するツイートのID</param>
1056         /// <param name="read">既読状態</param>
1057         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1058         public bool SetReadAllTab(long statusId, bool read)
1059         {
1060             lock (LockObj)
1061             {
1062                 foreach (var tab in this._tabs.Values)
1063                 {
1064                     if (!tab.Contains(statusId))
1065                         continue;
1066
1067                     tab.SetReadState(statusId, read);
1068                 }
1069
1070                 // TabInformations自身が保持しているツイートであればここで IsRead を変化させる
1071                 PostClass post;
1072                 if (this.Posts.TryGetValue(statusId, out post))
1073                     post.IsRead = read;
1074
1075                 return true;
1076             }
1077         }
1078
1079         /// <summary>
1080         /// Home タブのツイートを全て既読にします。
1081         /// ただし IsReply または FilterHit が true なものを除きます。
1082         /// </summary>
1083         public void SetReadHomeTab()
1084         {
1085             var homeTab = this.GetTabByType(MyCommon.TabUsageType.Home);
1086
1087             lock (LockObj)
1088             {
1089                 foreach (var statusId in homeTab.GetUnreadIds())
1090                 {
1091                     PostClass post;
1092                     if (!this.Posts.TryGetValue(statusId, out post))
1093                         continue;
1094
1095                     if (post.IsReply || post.FilterHit)
1096                         continue;
1097
1098                     this.SetReadAllTab(post.StatusId, read: true);
1099                 }
1100             }
1101         }
1102
1103         public PostClass this[long ID]
1104         {
1105             get
1106             {
1107                 PostClass status;
1108                 if (this._statuses.TryGetValue(ID, out status))
1109                     return status;
1110
1111                 if (this._retweets.TryGetValue(ID, out status))
1112                     return status;
1113
1114                 if (this._quotes.TryGetValue(ID, out status))
1115                     return status;
1116
1117                 return this.GetTabsInnerStorageType()
1118                     .Select(x => x.Posts.TryGetValue(ID, out status) ? status : null)
1119                     .Where(x => x != null)
1120                     .FirstOrDefault();
1121             }
1122         }
1123
1124         public bool ContainsKey(long Id)
1125         {
1126             //DM,公式検索は非対応
1127             lock (LockObj)
1128             {
1129                 return _statuses.ContainsKey(Id);
1130             }
1131         }
1132
1133         public void RenameTab(string Original, string NewName)
1134         {
1135             var tb = _tabs[Original];
1136             _tabs.Remove(Original);
1137             tb.TabName = NewName;
1138             _tabs.Add(NewName, tb);
1139         }
1140
1141         public void FilterAll()
1142         {
1143             lock (LockObj)
1144             {
1145                 var homeTab = GetTabByType(MyCommon.TabUsageType.Home);
1146                 var detachedIdsAll = Enumerable.Empty<long>();
1147
1148                 foreach (var tab in _tabs.Values.ToArray())
1149                 {
1150                     if (!tab.IsDistributableTabType)
1151                         continue;
1152
1153                     if (tab.TabType == MyCommon.TabUsageType.Mute)
1154                         continue;
1155
1156                     // フィルタに変更のあったタブのみを対象とする
1157                     if (!tab.FilterModified)
1158                         continue;
1159
1160                     tab.FilterModified = false;
1161
1162                     // フィルタ実行前の時点でタブに含まれていたstatusIdを記憶する
1163                     var orgIds = tab.BackupIds;
1164                     tab.ClearIDs();
1165
1166                     foreach (var post in _statuses.Values)
1167                     {
1168                         var filterHit = false; // フィルタにヒットしたタブがあるか
1169                         var mark = false; // フィルタによってマーク付けされたか
1170                         var excluded = false; // 除外フィルタによって除外されたか
1171                         var moved = false; // Recentタブから移動するか (Recentタブに表示しない)
1172
1173                         switch (tab.AddFiltered(post, immediately: true))
1174                         {
1175                             case MyCommon.HITRESULT.Copy:
1176                                 filterHit = true;
1177                                 break;
1178                             case MyCommon.HITRESULT.CopyAndMark:
1179                                 filterHit = true;
1180                                 mark = true;
1181                                 break;
1182                             case MyCommon.HITRESULT.Move:
1183                                 filterHit = true;
1184                                 moved = true;
1185                                 break;
1186                             case MyCommon.HITRESULT.None:
1187                                 break;
1188                             case MyCommon.HITRESULT.Exclude:
1189                                 excluded = true;
1190                                 break;
1191                         }
1192
1193                         post.FilterHit = filterHit;
1194                         post.IsMark = mark;
1195
1196                         // 移動されたらRecentから除去
1197                         if (moved)
1198                             homeTab.Remove(post.StatusId);
1199
1200                         if (tab.TabType == MyCommon.TabUsageType.Mentions)
1201                         {
1202                             post.IsExcludeReply = excluded;
1203
1204                             // 除外ルール適用のないReplyならReplyタブに追加
1205                             if (post.IsReply && !excluded)
1206                                 tab.AddPostImmediately(post.StatusId, post.IsRead);
1207                         }
1208                     }
1209
1210                     // フィルタの更新によってタブから取り除かれたツイートのID
1211                     var detachedIds = orgIds.Except(tab.BackupIds).ToArray();
1212
1213                     detachedIdsAll = detachedIdsAll.Concat(detachedIds);
1214                 }
1215
1216                 // detachedIdsAll のうち、最終的にどのタブにも振り分けられていないツイートがあればRecentに追加
1217                 foreach (var id in detachedIdsAll)
1218                 {
1219                     var hit = false;
1220                     foreach (var tbTemp in _tabs.Values.ToArray())
1221                     {
1222                         if (!tbTemp.IsDistributableTabType)
1223                             continue;
1224
1225                         if (tbTemp.Contains(id))
1226                         {
1227                             hit = true;
1228                             break;
1229                         }
1230                     }
1231
1232                     if (!hit)
1233                     {
1234                         PostClass post;
1235                         if (this._statuses.TryGetValue(id, out post))
1236                             homeTab.AddPostImmediately(post.StatusId, post.IsRead);
1237                     }
1238                 }
1239             }
1240         }
1241
1242         public void ClearTabIds(string TabName)
1243         {
1244             //不要なPostを削除
1245             lock (LockObj)
1246             {
1247                 var tb = _tabs[TabName];
1248                 if (!tb.IsInnerStorageTabType)
1249                 {
1250                     foreach (var Id in tb.BackupIds)
1251                     {
1252                         var Hit = false;
1253                         foreach (var tab in _tabs.Values)
1254                         {
1255                             if (tab.Contains(Id))
1256                             {
1257                                 Hit = true;
1258                                 break;
1259                             }
1260                         }
1261                         if (!Hit)
1262                         {
1263                             PostClass removedPost;
1264                             _statuses.TryRemove(Id, out removedPost);
1265                         }
1266                     }
1267                 }
1268
1269                 //指定タブをクリア
1270                 tb.ClearIDs();
1271             }
1272         }
1273
1274         public void RefreshOwl(ISet<long> follower)
1275         {
1276             lock (LockObj)
1277             {
1278                 if (follower.Count > 0)
1279                 {
1280                     foreach (var post in _statuses.Values)
1281                     {
1282                         //if (post.UserId = 0 || post.IsDm) Continue For
1283                         if (post.IsMe)
1284                         {
1285                             post.IsOwl = false;
1286                         }
1287                         else
1288                         {
1289                             post.IsOwl = !follower.Contains(post.UserId);
1290                         }
1291                     }
1292                 }
1293                 else
1294                 {
1295                     foreach (var post in _statuses.Values)
1296                     {
1297                         post.IsOwl = false;
1298                     }
1299                 }
1300             }
1301         }
1302
1303         public TabClass GetTabByType(MyCommon.TabUsageType tabType)
1304         {
1305             //Home,Mentions,DM,Favは1つに制限する
1306             //その他のタイプを指定されたら、最初に合致したものを返す
1307             //合致しなければnullを返す
1308             lock (LockObj)
1309             {
1310                 return this._tabs.Values
1311                     .FirstOrDefault(x => x.TabType.HasFlag(tabType));
1312             }
1313         }
1314
1315         public TabClass[] GetTabsByType(MyCommon.TabUsageType tabType)
1316         {
1317             lock (LockObj)
1318             {
1319                 return this._tabs.Values
1320                     .Where(x => x.TabType.HasFlag(tabType))
1321                     .ToArray();
1322             }
1323         }
1324
1325         public TabClass[] GetTabsInnerStorageType()
1326         {
1327             lock (LockObj)
1328             {
1329                 return this._tabs.Values
1330                     .Where(x => x.IsInnerStorageTabType)
1331                     .ToArray();
1332             }
1333         }
1334
1335         public TabClass GetTabByName(string tabName)
1336         {
1337             lock (LockObj)
1338             {
1339                 TabClass tab;
1340                 return _tabs.TryGetValue(tabName, out tab)
1341                     ? tab
1342                     : null;
1343             }
1344         }
1345
1346         public ConcurrentDictionary<long, PostClass> Posts
1347             => this._statuses;
1348     }
1349
1350     [Serializable]
1351     public sealed class TabClass
1352     {
1353         private List<PostFilterRule> _filters;
1354         private IndexedSortedSet<long> _ids;
1355         private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
1356         private SortedSet<long> unreadIds = new SortedSet<long>();
1357         private MyCommon.TabUsageType _tabType = MyCommon.TabUsageType.Undefined;
1358
1359         private readonly object _lockObj = new object();
1360
1361         public string User { get; set; }
1362
1363 #region "検索"
1364         //Search query
1365         private string _searchLang = "";
1366         private string _searchWords = "";
1367
1368         public string SearchLang
1369         {
1370             get
1371             {
1372                 return _searchLang;
1373             }
1374             set
1375             {
1376                 _searchLang = value;
1377                 this.ResetFetchIds();
1378             }
1379         }
1380         public string SearchWords
1381         {
1382             get
1383             {
1384                 return _searchWords;
1385             }
1386             set
1387             {
1388                 _searchWords = value.Trim();
1389                 this.ResetFetchIds();
1390             }
1391         }
1392
1393         private Dictionary<string, string> _beforeQuery = new Dictionary<string, string>();
1394
1395         public bool IsSearchQueryChanged
1396         {
1397             get
1398             {
1399                 var qry = new Dictionary<string, string>();
1400                 if (!string.IsNullOrEmpty(_searchWords))
1401                 {
1402                     qry.Add("q", _searchWords);
1403                     if (!string.IsNullOrEmpty(_searchLang)) qry.Add("lang", _searchLang);
1404                 }
1405                 if (qry.Count != _beforeQuery.Count)
1406                 {
1407                     _beforeQuery = qry;
1408                     return true;
1409                 }
1410
1411                 foreach (var kvp in qry)
1412                 {
1413                     string value;
1414                     if (!_beforeQuery.TryGetValue(kvp.Key, out value) || value != kvp.Value)
1415                     {
1416                         _beforeQuery = qry;
1417                         return true;
1418                     }
1419                 }
1420                 return false;
1421             }
1422         }
1423 #endregion
1424
1425 #region "リスト"
1426         [NonSerialized]
1427         private ListElement _listInfo;
1428         public ListElement ListInfo
1429         {
1430             get
1431             {
1432                 return _listInfo;
1433             }
1434             set
1435             {
1436                 _listInfo = value;
1437             }
1438         }
1439 #endregion
1440
1441         [XmlIgnore]
1442         public PostClass RelationTargetPost { get; set; }
1443
1444         [XmlIgnore]
1445         public long OldestId = long.MaxValue;
1446
1447         [XmlIgnore]
1448         public long SinceId { get; set; }
1449
1450         [XmlIgnore]
1451         public ConcurrentDictionary<long, PostClass> Posts { get; private set; }
1452
1453         private ConcurrentDictionary<long, PostClass> _innerPosts;
1454
1455         private struct TemporaryId
1456         {
1457             public long Id;
1458             public bool Read;
1459
1460             public TemporaryId(long argId, bool argRead)
1461             {
1462                 Id = argId;
1463                 Read = argRead;
1464             }
1465         }
1466
1467         public TabClass()
1468         {
1469             _innerPosts = new ConcurrentDictionary<long, PostClass>();
1470             Posts = _innerPosts;
1471             SoundFile = "";
1472             TabName = "";
1473             _filters = new List<PostFilterRule>();
1474             Protected = false;
1475             Notify = true;
1476             SoundFile = "";
1477             UnreadManage = true;
1478             _ids = new IndexedSortedSet<long>();
1479             _tabType = MyCommon.TabUsageType.Undefined;
1480             _listInfo = null;
1481         }
1482
1483         public TabClass(string TabName, MyCommon.TabUsageType TabType, ListElement list) : this()
1484         {
1485             this.TabName = TabName;
1486             this.TabType = TabType;
1487             this.ListInfo = list;
1488         }
1489
1490         /// <summary>
1491         /// タブ更新時に使用する SinceId, OldestId をリセットする
1492         /// </summary>
1493         public void ResetFetchIds()
1494         {
1495             this.SinceId = 0L;
1496             this.OldestId = long.MaxValue;
1497         }
1498
1499         /// <summary>
1500         /// ソート対象のフィールドとソート順を設定し、ソートを実行します
1501         /// </summary>
1502         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
1503         {
1504             this.SortMode = mode;
1505             this.SortOrder = sortOrder;
1506
1507             this.ApplySortMode();
1508         }
1509
1510         private void ApplySortMode()
1511         {
1512             var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
1513
1514             Comparison<long> comparison;
1515             if (this.SortMode == ComparerMode.Id)
1516             {
1517                 comparison = (x, y) => sign * x.CompareTo(y);
1518             }
1519             else
1520             {
1521                 Comparison<PostClass> postComparison;
1522                 switch (this.SortMode)
1523                 {
1524                     case ComparerMode.Data:
1525                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
1526                         break;
1527                     case ComparerMode.Name:
1528                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
1529                         break;
1530                     case ComparerMode.Nickname:
1531                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
1532                         break;
1533                     case ComparerMode.Source:
1534                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
1535                         break;
1536                     default:
1537                         throw new InvalidEnumArgumentException();
1538                 }
1539
1540                 comparison = (x, y) =>
1541                 {
1542                     PostClass xPost, yPost;
1543                     this.Posts.TryGetValue(x, out xPost);
1544                     this.Posts.TryGetValue(y, out yPost);
1545
1546                     var compare = sign * postComparison(xPost, yPost);
1547                     if (compare != 0)
1548                         return compare;
1549
1550                     // 同値であれば status_id で比較する
1551                     return sign * x.CompareTo(y);
1552                 };
1553             }
1554
1555             var comparer = Comparer<long>.Create(comparison);
1556
1557             this._ids = new IndexedSortedSet<long>(this._ids, comparer);
1558             this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
1559         }
1560
1561         [XmlIgnore]
1562         public ComparerMode SortMode { get; private set; }
1563
1564         [XmlIgnore]
1565         public SortOrder SortOrder { get; private set; }
1566
1567         public void AddPostQueue(long statusId, bool read)
1568         {
1569             this.addQueue.Enqueue(new TemporaryId(statusId, read));
1570         }
1571
1572         //無条件に追加
1573         public void AddPostImmediately(long ID, bool Read)
1574         {
1575             if (this._ids.Contains(ID)) return;
1576
1577             this._ids.Add(ID);
1578
1579             if (!Read)
1580                 this.unreadIds.Add(ID);
1581         }
1582
1583         //フィルタに合致したら追加
1584         public MyCommon.HITRESULT AddFiltered(PostClass post, bool immediately = false)
1585         {
1586             if (this.IsInnerStorageTabType) return MyCommon.HITRESULT.None;
1587
1588             var rslt = MyCommon.HITRESULT.None;
1589             //全フィルタ評価(優先順位あり)
1590             lock (this._lockObj)
1591             {
1592                 foreach (var ft in _filters)
1593                 {
1594                     try
1595                     {
1596                         switch (ft.ExecFilter(post))   //フィルタクラスでヒット判定
1597                         {
1598                             case MyCommon.HITRESULT.None:
1599                                 break;
1600                             case MyCommon.HITRESULT.Copy:
1601                                 if (rslt != MyCommon.HITRESULT.CopyAndMark) rslt = MyCommon.HITRESULT.Copy;
1602                                 break;
1603                             case MyCommon.HITRESULT.CopyAndMark:
1604                                 rslt = MyCommon.HITRESULT.CopyAndMark;
1605                                 break;
1606                             case MyCommon.HITRESULT.Move:
1607                                 rslt = MyCommon.HITRESULT.Move;
1608                                 break;
1609                             case MyCommon.HITRESULT.Exclude:
1610                                 rslt = MyCommon.HITRESULT.Exclude;
1611                                 goto exit_for;
1612                         }
1613                     }
1614                     catch (NullReferenceException)
1615                     {
1616                         // ExecFilterでNullRef出る場合あり。暫定対応
1617                         MyCommon.TraceOut("ExecFilterでNullRef: " + ft.ToString());
1618                         rslt = MyCommon.HITRESULT.None;
1619                     }
1620                 }
1621             exit_for:
1622                 ;
1623             }
1624
1625             if (this.TabType != MyCommon.TabUsageType.Mute &&
1626                 rslt != MyCommon.HITRESULT.None && rslt != MyCommon.HITRESULT.Exclude)
1627             {
1628                 if (immediately)
1629                     this.AddPostImmediately(post.StatusId, post.IsRead);
1630                 else
1631                     this.AddPostQueue(post.StatusId, post.IsRead);
1632             }
1633
1634             return rslt; //マーク付けは呼び出し元で行うこと
1635         }
1636
1637         //検索結果の追加
1638         public void AddPostToInnerStorage(PostClass Post)
1639         {
1640             if (_innerPosts.ContainsKey(Post.StatusId)) return;
1641             _innerPosts.TryAdd(Post.StatusId, Post);
1642             this.AddPostQueue(Post.StatusId, Post.IsRead);
1643         }
1644
1645         public IList<long> AddSubmit()
1646         {
1647             var addedIds = new List<long>();
1648
1649             TemporaryId tId;
1650             while (this.addQueue.TryDequeue(out tId))
1651             {
1652                 this.AddPostImmediately(tId.Id, tId.Read);
1653                 addedIds.Add(tId.Id);
1654             }
1655
1656             return addedIds;
1657         }
1658
1659         public void Remove(long Id)
1660         {
1661             if (!this._ids.Contains(Id))
1662                 return;
1663
1664             this._ids.Remove(Id);
1665             this.unreadIds.Remove(Id);
1666
1667             if (this.IsInnerStorageTabType)
1668             {
1669                 PostClass removedPost;
1670                 this._innerPosts.TryRemove(Id, out removedPost);
1671             }
1672         }
1673
1674         public bool UnreadManage { get; set; }
1675
1676         // v1.0.5で「タブを固定(Locked)」から「タブを保護(Protected)」に名称変更
1677         [XmlElement(ElementName = "Locked")]
1678         public bool Protected { get; set; }
1679
1680         public bool Notify { get; set; }
1681
1682         public string SoundFile { get; set; }
1683
1684         /// <summary>
1685         /// 次に表示する未読ツイートのIDを返します。
1686         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1687         /// </summary>
1688         [XmlIgnore]
1689         public long NextUnreadId
1690         {
1691             get
1692             {
1693                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1694                     return -1L;
1695
1696                 if (this.unreadIds.Count == 0)
1697                     return -1L;
1698
1699                 // unreadIds はリストのインデックス番号順に並んでいるため、
1700                 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
1701                 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
1702             }
1703         }
1704
1705         /// <summary>
1706         /// 次に表示する未読ツイートのインデックス番号を返します。
1707         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
1708         /// </summary>
1709         [XmlIgnore]
1710         public int NextUnreadIndex
1711         {
1712             get
1713             {
1714                 var unreadId = this.NextUnreadId;
1715                 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
1716             }
1717         }
1718
1719         /// <summary>
1720         /// 未読ツイートの件数を返します。
1721         /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
1722         /// </summary>
1723         [XmlIgnore]
1724         public int UnreadCount
1725         {
1726             get
1727             {
1728                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
1729                     return 0;
1730
1731                 return this.unreadIds.Count;
1732             }
1733         }
1734
1735         public int AllCount
1736         {
1737             get
1738             {
1739                 return this._ids.Count;
1740             }
1741         }
1742
1743         /// <summary>
1744         /// 未読ツイートの ID を配列で返します
1745         /// </summary>
1746         public long[] GetUnreadIds()
1747         {
1748             lock (this._lockObj)
1749             {
1750                 return this.unreadIds.ToArray();
1751             }
1752         }
1753
1754         /// <summary>
1755         /// タブ内の既読状態を変更します
1756         /// </summary>
1757         /// <remarks>
1758         /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
1759         /// </remarks>
1760         /// <param name="statusId">変更するツイートのID</param>
1761         /// <param name="read">既読状態</param>
1762         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
1763         internal bool SetReadState(long statusId, bool read)
1764         {
1765             if (!this._ids.Contains(statusId))
1766                 throw new ArgumentException(nameof(statusId));
1767
1768             if (this.IsInnerStorageTabType)
1769                 this.Posts[statusId].IsRead = read;
1770
1771             if (read)
1772                 return this.unreadIds.Remove(statusId);
1773             else
1774                 return this.unreadIds.Add(statusId);
1775         }
1776
1777         public PostFilterRule[] GetFilters()
1778         {
1779             lock (this._lockObj)
1780             {
1781                 return _filters.ToArray();
1782             }
1783         }
1784
1785         public void RemoveFilter(PostFilterRule filter)
1786         {
1787             lock (this._lockObj)
1788             {
1789                 _filters.Remove(filter);
1790                 filter.PropertyChanged -= this.OnFilterModified;
1791                 this.FilterModified = true;
1792             }
1793         }
1794
1795         public bool AddFilter(PostFilterRule filter)
1796         {
1797             lock (this._lockObj)
1798             {
1799                 if (_filters.Contains(filter)) return false;
1800                 filter.PropertyChanged += this.OnFilterModified;
1801                 _filters.Add(filter);
1802                 this.FilterModified = true;
1803                 return true;
1804             }
1805         }
1806
1807         private void OnFilterModified(object sender, PropertyChangedEventArgs e)
1808         {
1809             this.FilterModified = true;
1810         }
1811
1812         public PostFilterRule[] FilterArray
1813         {
1814             get
1815             {
1816                 lock (this._lockObj)
1817                 {
1818                     return _filters.ToArray();
1819                 }
1820             }
1821             set
1822             {
1823                 lock (this._lockObj)
1824                 {
1825                     foreach (var oldFilter in this._filters)
1826                     {
1827                         oldFilter.PropertyChanged -= this.OnFilterModified;
1828                     }
1829
1830                     this._filters.Clear();
1831                     this.FilterModified = true;
1832
1833                     foreach (var newFilter in value)
1834                     {
1835                         _filters.Add(newFilter);
1836                         newFilter.PropertyChanged += this.OnFilterModified;
1837                     }
1838                 }
1839             }
1840         }
1841         public bool Contains(long ID)
1842         {
1843             return _ids.Contains(ID);
1844         }
1845
1846         public void ClearIDs()
1847         {
1848             _ids.Clear();
1849             this.unreadIds.Clear();
1850             _innerPosts.Clear();
1851
1852             Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
1853         }
1854
1855         public PostClass this[int Index]
1856         {
1857             get
1858             {
1859                 var id = GetId(Index);
1860                 if (id < 0) throw new ArgumentException("Index can't find. Index=" + Index.ToString() + "/TabName=" + TabName, nameof(Index));
1861                 return Posts[id];
1862             }
1863         }
1864
1865         public PostClass[] this[int StartIndex, int EndIndex]
1866         {
1867             get
1868             {
1869                 var length = EndIndex - StartIndex + 1;
1870                 var posts = new PostClass[length];
1871                 for (int i = 0; i < length; i++)
1872                 {
1873                     posts[i] = Posts[GetId(StartIndex + i)];
1874                 }
1875                 return posts;
1876             }
1877         }
1878
1879         public long[] GetId(ListView.SelectedIndexCollection IndexCollection)
1880         {
1881             if (IndexCollection.Count == 0) return null;
1882
1883             var Ids = new long[IndexCollection.Count];
1884             for (int i = 0; i < Ids.Length; i++)
1885             {
1886                 Ids[i] = GetId(IndexCollection[i]);
1887             }
1888             return Ids;
1889         }
1890
1891         public long GetId(int Index)
1892         {
1893             return Index < _ids.Count ? _ids[Index] : -1;
1894         }
1895
1896         public int[] IndexOf(long[] Ids)
1897         {
1898             if (Ids == null) return null;
1899             var idx = new int[Ids.Length];
1900             for (int i = 0; i < Ids.Length; i++)
1901             {
1902                 idx[i] = IndexOf(Ids[i]);
1903             }
1904             return idx;
1905         }
1906
1907         public int IndexOf(long ID)
1908         {
1909             return _ids.IndexOf(ID);
1910         }
1911
1912         [XmlIgnore]
1913         public bool FilterModified { get; set; }
1914
1915         public long[] BackupIds
1916         {
1917             get
1918             {
1919                 return _ids.ToArray();
1920             }
1921         }
1922
1923         public string TabName { get; set; }
1924
1925         public MyCommon.TabUsageType TabType
1926         {
1927             get
1928             {
1929                 return _tabType;
1930             }
1931             set
1932             {
1933                 _tabType = value;
1934                 if (this.IsInnerStorageTabType)
1935                 {
1936                     Posts = _innerPosts;
1937                 }
1938                 else
1939                 {
1940                     Posts = TabInformations.GetInstance().Posts;
1941                 }
1942             }
1943         }
1944
1945         public bool IsDefaultTabType
1946         {
1947             get
1948             {
1949                 return _tabType.IsDefault();
1950             }
1951         }
1952
1953         public bool IsDistributableTabType
1954         {
1955             get
1956             {
1957                 return _tabType.IsDistributable();
1958             }
1959         }
1960
1961         public bool IsInnerStorageTabType
1962         {
1963             get
1964             {
1965                 return _tabType.IsInnerStorage();
1966             }
1967         }
1968     }
1969
1970     /// <summary>
1971     /// enum TabUsageType に対応する拡張メソッドを定義したクラス
1972     /// </summary>
1973     public static class TabUsageTypeExt
1974     {
1975         const MyCommon.TabUsageType DefaultTabTypeMask =
1976             MyCommon.TabUsageType.Home |
1977             MyCommon.TabUsageType.Mentions |
1978             MyCommon.TabUsageType.DirectMessage |
1979             MyCommon.TabUsageType.Favorites |
1980             MyCommon.TabUsageType.Mute;
1981
1982         const MyCommon.TabUsageType DistributableTabTypeMask =
1983             MyCommon.TabUsageType.Mentions |
1984             MyCommon.TabUsageType.UserDefined |
1985             MyCommon.TabUsageType.Mute;
1986
1987         const MyCommon.TabUsageType InnerStorageTabTypeMask =
1988             MyCommon.TabUsageType.DirectMessage |
1989             MyCommon.TabUsageType.PublicSearch |
1990             MyCommon.TabUsageType.Lists |
1991             MyCommon.TabUsageType.UserTimeline |
1992             MyCommon.TabUsageType.Related |
1993             MyCommon.TabUsageType.SearchResults;
1994
1995         /// <summary>
1996         /// デフォルトタブかどうかを示す値を取得します。
1997         /// </summary>
1998         public static bool IsDefault(this MyCommon.TabUsageType tabType)
1999         {
2000             return (tabType & DefaultTabTypeMask) != 0;
2001         }
2002
2003         /// <summary>
2004         /// 振り分け可能タブかどうかを示す値を取得します。
2005         /// </summary>
2006         public static bool IsDistributable(this MyCommon.TabUsageType tabType)
2007         {
2008             return (tabType & DistributableTabTypeMask) != 0;
2009         }
2010
2011         /// <summary>
2012         /// 内部ストレージを使用するタブかどうかを示す値を取得します。
2013         /// </summary>
2014         public static bool IsInnerStorage(this MyCommon.TabUsageType tabType)
2015         {
2016             return (tabType & InnerStorageTabTypeMask) != 0;
2017         }
2018     }
2019
2020     /// <summary>
2021     /// 比較する方法
2022     /// </summary>
2023     public enum ComparerMode
2024     {
2025         Id,
2026         Data,
2027         Name,
2028         Nickname,
2029         Source,
2030     }
2031 }