1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
29 using System.Collections.Concurrent;
30 using System.Collections.Generic;
33 using System.Threading;
34 using System.Threading.Tasks;
35 using System.Windows.Forms;
36 using OpenTween.Setting;
38 namespace OpenTween.Models
40 public abstract class TabModel
42 public string TabName { get; set; }
44 public bool UnreadManage { get; set; } = true;
45 public bool Protected { get; set; }
46 public bool Notify { get; set; } = true;
47 public string SoundFile { get; set; } = "";
49 public ComparerMode SortMode { get; private set; }
50 public SortOrder SortOrder { get; private set; }
52 public long OldestId { get; set; } = long.MaxValue;
53 public long SinceId { get; set; }
55 public abstract MyCommon.TabUsageType TabType { get; }
57 public virtual ConcurrentDictionary<long, PostClass> Posts
58 => TabInformations.GetInstance().Posts;
60 public int AllCount => this._ids.Count;
61 public long[] StatusIds => this._ids.ToArray();
63 public bool IsDefaultTabType => this.TabType.IsDefault();
64 public bool IsDistributableTabType => this.TabType.IsDistributable();
65 public bool IsInnerStorageTabType => this.TabType.IsInnerStorage();
68 /// 次回起動時にも保持されるタブか(SettingTabsに保存されるか)
70 public virtual bool IsPermanentTabType => true;
72 public long[] SelectedStatusIds
73 => this.selectedStatusIds.ToArray();
75 public long SelectedStatusId
76 => this.selectedStatusIds.DefaultIfEmpty(-1).First();
78 public PostClass[] SelectedPosts
79 => this.selectedStatusIds.Select(x => this.Posts[x]).ToArray();
81 public PostClass SelectedPost
82 => this.selectedStatusIds.Select(x => this.Posts[x]).FirstOrDefault();
84 public int SelectedIndex
88 var statusId = this.SelectedStatusId;
89 return statusId != -1 ? this.IndexOf(statusId) : -1;
93 private IndexedSortedSet<long> _ids = new IndexedSortedSet<long>();
94 private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
95 private ConcurrentQueue<long> removeQueue = new ConcurrentQueue<long>();
96 private SortedSet<long> unreadIds = new SortedSet<long>();
97 private List<long> selectedStatusIds = new List<long>();
99 private readonly object _lockObj = new object();
101 protected TabModel(string tabName)
102 => this.TabName = tabName;
104 public abstract Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress<string> progress);
106 private struct TemporaryId
108 public long StatusId { get; }
109 public bool Read { get; }
111 public TemporaryId(long statusId, bool read)
113 this.StatusId = statusId;
118 public virtual void AddPostQueue(PostClass post)
120 if (!this.Posts.ContainsKey(post.StatusId))
121 throw new ArgumentException("Specified post not exists in storage", nameof(post));
123 this.addQueue.Enqueue(new TemporaryId(post.StatusId, post.IsRead));
127 internal bool AddPostImmediately(long statusId, bool read)
129 if (!this._ids.Add(statusId))
133 this.unreadIds.Add(statusId);
138 public IReadOnlyList<long> AddSubmit()
140 var addedIds = new List<long>();
142 while (this.addQueue.TryDequeue(out var tId))
144 if (this.AddPostImmediately(tId.StatusId, tId.Read))
145 addedIds.Add(tId.StatusId);
151 public virtual void EnqueueRemovePost(long statusId, bool setIsDeleted)
152 => this.removeQueue.Enqueue(statusId);
154 public virtual bool RemovePostImmediately(long statusId)
156 if (!this._ids.Remove(statusId))
159 this.unreadIds.Remove(statusId);
160 this.selectedStatusIds.Remove(statusId);
164 public IReadOnlyList<long> RemoveSubmit()
166 var removedIds = new List<long>();
168 while (this.removeQueue.TryDequeue(out var statusId))
170 if (this.RemovePostImmediately(statusId))
171 removedIds.Add(statusId);
177 public void SelectPosts(int[] indices)
179 bool IsValidIndex(int index)
180 => index >= 0 && index < this.AllCount;
182 var firstErrorId = indices.FirstOrDefault(x => !IsValidIndex(x));
183 if (firstErrorId != default)
184 throw new ArgumentOutOfRangeException($"Invalid index: {firstErrorId}", nameof(indices));
186 var statusIds = indices.Select(x => this.GetStatusIdAt(x)).ToList();
187 this.selectedStatusIds = statusIds;
190 public virtual void ClearIDs()
193 this.unreadIds.Clear();
194 this.selectedStatusIds.Clear();
196 Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
200 /// タブ更新時に使用する SinceId, OldestId をリセットする
202 public void ResetFetchIds()
205 this.OldestId = long.MaxValue;
209 /// ソート対象のフィールドとソート順を設定し、ソートを実行します
211 public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
213 this.SortMode = mode;
214 this.SortOrder = sortOrder;
216 this.ApplySortMode();
219 private void ApplySortMode()
221 var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
223 Comparison<long> comparison;
224 if (this.SortMode == ComparerMode.Id)
226 comparison = (x, y) => sign * x.CompareTo(y);
230 Comparison<PostClass> postComparison;
231 switch (this.SortMode)
234 case ComparerMode.Data:
235 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
237 case ComparerMode.Name:
238 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
240 case ComparerMode.Nickname:
241 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
243 case ComparerMode.Source:
244 postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
248 comparison = (x, y) =>
250 this.Posts.TryGetValue(x, out var xPost);
251 this.Posts.TryGetValue(y, out var yPost);
253 var compare = sign * postComparison(xPost, yPost);
257 // 同値であれば status_id で比較する
258 return sign * x.CompareTo(y);
262 var comparer = Comparer<long>.Create(comparison);
264 this._ids = new IndexedSortedSet<long>(this._ids, comparer);
265 this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
269 /// 次に表示する未読ツイートのIDを返します。
270 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
272 public long NextUnreadId
276 if (!this.UnreadManage || !SettingManager.Common.UnreadManage)
279 if (this.unreadIds.Count == 0)
282 // unreadIds はリストのインデックス番号順に並んでいるため、
283 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
284 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
289 /// 次に表示する未読ツイートのインデックス番号を返します。
290 /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
292 public int NextUnreadIndex
296 var unreadId = this.NextUnreadId;
297 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
303 /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
305 public int UnreadCount
309 if (!this.UnreadManage || !SettingManager.Common.UnreadManage)
312 return this.unreadIds.Count;
317 /// 未読ツイートの ID を配列で返します
319 public long[] GetUnreadIds()
322 return this.unreadIds.ToArray();
329 /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
331 /// <param name="statusId">変更するツイートのID</param>
332 /// <param name="read">既読状態</param>
333 /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
334 internal virtual bool SetReadState(long statusId, bool read)
336 if (!this._ids.Contains(statusId))
337 throw new ArgumentOutOfRangeException(nameof(statusId));
340 return this.unreadIds.Remove(statusId);
342 return this.unreadIds.Add(statusId);
345 public bool Contains(long statusId)
346 => this._ids.Contains(statusId);
348 public PostClass this[int index]
352 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out var post))
353 throw new ArgumentOutOfRangeException(nameof(index), "Post not exists");
359 public PostClass[] this[int startIndex, int endIndex]
364 throw new ArgumentOutOfRangeException(nameof(startIndex));
365 if (endIndex >= this.AllCount)
366 throw new ArgumentOutOfRangeException(nameof(endIndex));
367 if (startIndex > endIndex)
368 throw new ArgumentException($"{nameof(startIndex)} must be equal to or less than {nameof(endIndex)}.", nameof(startIndex));
370 var length = endIndex - startIndex + 1;
371 var posts = new PostClass[length];
374 foreach (var idx in Enumerable.Range(startIndex, length))
376 var statusId = this.GetStatusIdAt(idx);
377 this.Posts.TryGetValue(statusId, out posts[i++]);
384 public long[] GetStatusIdAt(IEnumerable<int> indexes)
385 => indexes.Select(x => this.GetStatusIdAt(x)).ToArray();
387 public long GetStatusIdAt(int index)
390 public int[] IndexOf(long[] statusIds)
392 if (statusIds == null)
393 throw new ArgumentNullException(nameof(statusIds));
395 return statusIds.Select(x => this.IndexOf(x)).ToArray();
398 public int IndexOf(long statusId)
399 => this._ids.IndexOf(statusId);
401 public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer)
402 => this.SearchPostsAll(stringComparer, reverse: false);
404 public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex)
405 => this.SearchPostsAll(stringComparer, startIndex, reverse: false);
407 public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, bool reverse)
409 var startIndex = reverse ? this.AllCount - 1 : 0;
411 return this.SearchPostsAll(stringComparer, startIndex, reverse: false);
415 /// タブ内の発言を指定された条件で検索します
417 /// <param name="stringComparer">発言内容、スクリーン名、名前と比較する条件。マッチしたら true を返す</param>
418 /// <param name="startIndex">検索を開始する位置</param>
419 /// <param name="reverse">インデックスの昇順に検索する場合は false、降順の場合は true</param>
420 /// <returns></returns>
421 public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex, bool reverse)
423 if (this.AllCount == 0)
426 IEnumerable<int> searchIndices;
429 searchIndices = MyCommon.CircularCountUp(this.AllCount, startIndex);
431 searchIndices = MyCommon.CircularCountDown(this.AllCount, startIndex);
433 foreach (var index in searchIndices)
435 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out var post))
438 if (stringComparer(post.Nickname) || stringComparer(post.TextFromApi) || stringComparer(post.ScreenName))