diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 3144b0c91..27ad41e79 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -104,7 +104,7 @@ public async Task StatusesHomeTimeline_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesHomeTimeline(200, maxId: 900L, sinceId: 100L) + await twitterApi.StatusesHomeTimeline(200, maxId: new("900"), sinceId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -133,7 +133,7 @@ public async Task StatusesMentionsTimeline_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesMentionsTimeline(200, maxId: 900L, sinceId: 100L) + await twitterApi.StatusesMentionsTimeline(200, maxId: new("900"), sinceId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -164,7 +164,7 @@ public async Task StatusesUserTimeline_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesUserTimeline("twitterapi", count: 200, maxId: 900L, sinceId: 100L) + await twitterApi.StatusesUserTimeline("twitterapi", count: 200, maxId: new("900"), sinceId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -366,7 +366,7 @@ public async Task SearchTweets_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.SearchTweets("from:twitterapi", "en", count: 200, maxId: 900L, sinceId: 100L) + await twitterApi.SearchTweets("from:twitterapi", "en", count: 200, maxId: new("900"), sinceId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -553,7 +553,7 @@ public async Task ListsStatuses_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsStatuses(12345L, count: 200, maxId: 900L, sinceId: 100L, includeRTs: true) + await twitterApi.ListsStatuses(12345L, count: 200, maxId: new("900"), sinceId: new("100"), includeRTs: true) .ConfigureAwait(false); mock.VerifyAll(); diff --git a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs index 82ed7e59a..bf9900609 100644 --- a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs +++ b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs @@ -53,8 +53,8 @@ public async Task StatusesMentionsTimeline_Test() var request = new GetTimelineRequest(userId: 100L) { MaxResults = 200, - SinceId = "100", - UntilId = "900", + SinceId = new("100"), + UntilId = new("900"), }; await request.Send(mock.Object).ConfigureAwait(false); diff --git a/OpenTween.Tests/Models/PostIdTest.cs b/OpenTween.Tests/Models/PostIdTest.cs index 7ac665c87..33f67026d 100644 --- a/OpenTween.Tests/Models/PostIdTest.cs +++ b/OpenTween.Tests/Models/PostIdTest.cs @@ -59,6 +59,40 @@ public void CompareTo_SameIdTypeTest() Assert.Equal(0, a.CompareTo(a)); } + [Fact] + public void OperatorGreaterThan_Test() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "200"); +#pragma warning disable CS1718 + Assert.False(a < a); + Assert.True(a < b); + Assert.False(b < a); + Assert.False(b < b); + Assert.True(a <= a); + Assert.True(a <= b); + Assert.False(b <= a); + Assert.True(b <= b); +#pragma warning restore CS1718 + } + + [Fact] + public void OperatorLessThan_Test() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "200"); +#pragma warning disable CS1718 + Assert.False(a > a); + Assert.False(a > b); + Assert.True(b > a); + Assert.False(b > b); + Assert.True(a >= a); + Assert.False(a >= b); + Assert.True(b >= a); + Assert.True(b >= b); +#pragma warning restore CS1718 + } + [Fact] public void Equals_Test() { diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index 1c95a4086..c12a7d044 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -654,5 +654,140 @@ public void ParseDateTimeFromSnowflakeId_FallbackTest() var expected = new DateTimeUtc(2006, 3, 21, 20, 50, 14, 0); Assert.Equal(expected, TwitterPostFactory.ParseDateTimeFromSnowflakeId(statusId, createdAtStr)); } + + [Fact] + public void AdjustSortKeyForPromotedPost_BetweenNormatPostsTest() + { + // プロモーション以外のツイートは投稿日時の降順に並んでいると仮定する + var posts = new[] + { + new PostClass + { + IsPromoted = false, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 6), + }, + // 中間にあるプロモーションツイートは前後のツイートの中間に配置されるように調整する + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 7, 31, 0, 0, 0, 0), + }, + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 9, 22, 0, 0, 0, 0), + }, + new PostClass + { + IsPromoted = false, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 0), + }, + }; + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 6), posts[0].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 4), posts[1].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 2), posts[2].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 0), posts[3].CreatedAtForSorting); + } + + [Fact] + public void AdjustSortKeyForPromotedPost_BeforeNormatPostsTest() + { + // プロモーション以外のツイートは投稿日時の降順に並んでいると仮定する + var posts = new[] + { + // 先頭にあるプロモーションツイートは最初の通常ツイートを基準に 1ms ずつ加算した値をセットする + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 7, 31, 0, 0, 0, 0), + }, + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 9, 22, 0, 0, 0, 0), + }, + new PostClass + { + IsPromoted = false, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 0), + }, + }; + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 2), posts[0].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 1), posts[1].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 0), posts[2].CreatedAtForSorting); + } + + [Fact] + public void AdjustSortKeyForPromotedPost_AfterNormatPostsTest() + { + // プロモーション以外のツイートは投稿日時の降順に並んでいると仮定する + var posts = new[] + { + new PostClass + { + IsPromoted = false, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 6), + }, + // 末尾にあるプロモーションツイートは最後の通常ツイートを基準に 1ms ずつ減算した値をセットする + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 7, 31, 0, 0, 0, 0), + }, + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 9, 22, 0, 0, 0, 0), + }, + }; + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 6), posts[0].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 5), posts[1].CreatedAtForSorting); + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 4), posts[2].CreatedAtForSorting); + } + + [Fact] + public void AdjustSortKeyForPromotedPost_NormatPostsOnlyTest() + { + // プロモーションツイートが一件もない場合も正常に動くことをテストする + var posts = new[] + { + new PostClass + { + IsPromoted = false, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 0), + }, + }; + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 0), posts[0].CreatedAtForSorting); + } + + [Fact] + public void AdjustSortKeyForPromotedPost_PromotedPostsOnlyTest() + { + // プロモーションツイートしか存在しない場合も正常に動くことをテストする + var posts = new[] + { + new PostClass + { + IsPromoted = true, + CreatedAtForSorting = new(2023, 8, 1, 0, 0, 0, 0), + }, + }; + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + Assert.Equal(new(2023, 8, 1, 0, 0, 0, 0), posts[0].CreatedAtForSorting); + } } } diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 30b8cfb7c..157cc2d73 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -75,7 +75,7 @@ public void Initialize(TwitterAppToken appToken, string accessToken, string acce this.CurrentScreenName = screenName; } - public Task StatusesHomeTimeline(int? count = null, long? maxId = null, long? sinceId = null) + public Task StatusesHomeTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative); var param = new Dictionary @@ -88,14 +88,14 @@ public Task StatusesHomeTimeline(int? count = null, long? maxId if (count != null) param["count"] = count.ToString(); if (maxId != null) - param["max_id"] = maxId.ToString(); + param["max_id"] = maxId.Id; if (sinceId != null) - param["since_id"] = sinceId.ToString(); + param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/home_timeline"); } - public Task StatusesMentionsTimeline(int? count = null, long? maxId = null, long? sinceId = null) + public Task StatusesMentionsTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative); var param = new Dictionary @@ -108,14 +108,14 @@ public Task StatusesMentionsTimeline(int? count = null, long? m if (count != null) param["count"] = count.ToString(); if (maxId != null) - param["max_id"] = maxId.ToString(); + param["max_id"] = maxId.Id; if (sinceId != null) - param["since_id"] = sinceId.ToString(); + param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/mentions_timeline"); } - public Task StatusesUserTimeline(string screenName, int? count = null, long? maxId = null, long? sinceId = null) + public Task StatusesUserTimeline(string screenName, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("statuses/user_timeline.json", UriKind.Relative); var param = new Dictionary @@ -130,9 +130,9 @@ public Task StatusesUserTimeline(string screenName, int? count if (count != null) param["count"] = count.ToString(); if (maxId != null) - param["max_id"] = maxId.ToString(); + param["max_id"] = maxId.Id; if (sinceId != null) - param["since_id"] = sinceId.ToString(); + param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/statuses/user_timeline"); } @@ -221,7 +221,7 @@ public Task> StatusesRetweet(TwitterStatusId statusId) return this.Connection.PostLazyAsync(endpoint, param); } - public Task SearchTweets(string query, string? lang = null, int? count = null, long? maxId = null, long? sinceId = null) + public Task SearchTweets(string query, string? lang = null, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { var endpoint = new Uri("search/tweets.json", UriKind.Relative); var param = new Dictionary @@ -238,9 +238,9 @@ public Task SearchTweets(string query, string? lang = null, if (count != null) param["count"] = count.ToString(); if (maxId != null) - param["max_id"] = maxId.ToString(); + param["max_id"] = maxId.Id; if (sinceId != null) - param["since_id"] = sinceId.ToString(); + param["since_id"] = sinceId.Id; return this.Connection.GetAsync(endpoint, param, "/search/tweets"); } @@ -340,7 +340,7 @@ public Task> ListsDestroy(long listId) return this.Connection.PostLazyAsync(endpoint, param); } - public Task ListsStatuses(long listId, int? count = null, long? maxId = null, long? sinceId = null, bool? includeRTs = null) + public Task ListsStatuses(long listId, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null, bool? includeRTs = null) { var endpoint = new Uri("lists/statuses.json", UriKind.Relative); var param = new Dictionary @@ -354,9 +354,9 @@ public Task ListsStatuses(long listId, int? count = null, long? if (count != null) param["count"] = count.ToString(); if (maxId != null) - param["max_id"] = maxId.ToString(); + param["max_id"] = maxId.Id; if (sinceId != null) - param["since_id"] = sinceId.ToString(); + param["since_id"] = sinceId.Id; if (includeRTs != null) param["include_rts"] = includeRTs.Value ? "true" : "false"; diff --git a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs index fcc537d6b..6ff98cf55 100644 --- a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs +++ b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs @@ -28,6 +28,7 @@ using System.Threading.Tasks; using OpenTween.Api.DataModel; using OpenTween.Connection; +using OpenTween.Models; namespace OpenTween.Api.TwitterV2 { @@ -39,9 +40,9 @@ public class GetTimelineRequest public int? MaxResults { get; set; } - public string? UntilId { get; set; } + public TwitterStatusId? UntilId { get; set; } - public string? SinceId { get; set; } + public TwitterStatusId? SinceId { get; set; } public GetTimelineRequest(long userId) => this.UserId = userId; @@ -60,10 +61,10 @@ private Dictionary CreateParameters() param["max_results"] = this.MaxResults.ToString(); if (this.UntilId != null) - param["until_id"] = this.UntilId; + param["until_id"] = this.UntilId.Id; if (this.SinceId != null) - param["since_id"] = this.SinceId; + param["since_id"] = this.SinceId.Id; return param; } diff --git a/OpenTween/Models/HomeTabModel.cs b/OpenTween/Models/HomeTabModel.cs index 2a666ecb9..9d1f6fa0a 100644 --- a/OpenTween/Models/HomeTabModel.cs +++ b/OpenTween/Models/HomeTabModel.cs @@ -43,7 +43,7 @@ public class HomeTabModel : TabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.Home; - public long OldestId { get; set; } = long.MaxValue; + public PostId? OldestId { get; set; } public int TweetsPerHour => this.tweetsPerHour; diff --git a/OpenTween/Models/ListTimelineTabModel.cs b/OpenTween/Models/ListTimelineTabModel.cs index d1658a6be..c792a4029 100644 --- a/OpenTween/Models/ListTimelineTabModel.cs +++ b/OpenTween/Models/ListTimelineTabModel.cs @@ -43,7 +43,7 @@ public override MyCommon.TabUsageType TabType public ListElement ListInfo { get; set; } - public long OldestId { get; set; } = long.MaxValue; + public PostId? OldestId { get; set; } public string? CursorBottom { get; set; } diff --git a/OpenTween/Models/MentionsTabModel.cs b/OpenTween/Models/MentionsTabModel.cs index e3c410dbe..1792b160f 100644 --- a/OpenTween/Models/MentionsTabModel.cs +++ b/OpenTween/Models/MentionsTabModel.cs @@ -41,7 +41,7 @@ public class MentionsTabModel : FilterTabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.Mentions; - public long OldestId { get; set; } = long.MaxValue; + public PostId? OldestId { get; set; } public MentionsTabModel() : this(MyCommon.DEFAULTTAB.REPLY) diff --git a/OpenTween/Models/PostId.cs b/OpenTween/Models/PostId.cs index 82b36654a..da60a81ea 100644 --- a/OpenTween/Models/PostId.cs +++ b/OpenTween/Models/PostId.cs @@ -64,5 +64,17 @@ public override string ToString() public static bool operator !=(PostId? left, PostId? right) => !EqualityComparer.Default.Equals(left, right); + + public static bool operator <(PostId left, PostId right) + => Comparer.Default.Compare(left, right) < 0; + + public static bool operator <=(PostId left, PostId right) + => Comparer.Default.Compare(left, right) <= 0; + + public static bool operator >=(PostId left, PostId right) + => Comparer.Default.Compare(left, right) >= 0; + + public static bool operator >(PostId left, PostId right) + => Comparer.Default.Compare(left, right) > 0; } } diff --git a/OpenTween/Models/PublicSearchTabModel.cs b/OpenTween/Models/PublicSearchTabModel.cs index 7d47190f5..ce609f352 100644 --- a/OpenTween/Models/PublicSearchTabModel.cs +++ b/OpenTween/Models/PublicSearchTabModel.cs @@ -41,9 +41,9 @@ public class PublicSearchTabModel : InternalStorageTabModel public override MyCommon.TabUsageType TabType => MyCommon.TabUsageType.PublicSearch; - public long OldestId { get; set; } = long.MaxValue; + public PostId? OldestId { get; set; } - public long SinceId { get; set; } + public PostId? SinceId { get; set; } public string? CursorBottom { get; set; } @@ -101,8 +101,8 @@ await tw.GetSearch(read, this, backward) /// public void ResetFetchIds() { - this.SinceId = 0L; - this.OldestId = long.MaxValue; + this.SinceId = null; + this.OldestId = null; } } } diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index 0a36b31d1..6fb82e1b4 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -544,5 +544,87 @@ public static DateTimeUtc ParseDateTimeFromSnowflakeId(long statusId, string cre return correct ? timestampInMs : createdAtFromStr; } + + /// + /// Promotion Tweet のソート順を調整する + /// + /// + /// API から返ってきた並び順と同等となるように の値を調整する + /// + public static void AdjustSortKeyForPromotedPost(PostClass[] posts) + { + var firstSeenNormalPostIndex = posts.FindIndex(x => !x.IsPromoted); + + // 通常のツイートが一件も無い場合は何もせず終了(比較対象となるツイートが存在しないため) + if (firstSeenNormalPostIndex == -1) + return; + + // 通常のツイートより手前にプロモーションが挿入されていた場合は、 + // 最初の通常ツイートに対して 1ms ずつ加算した値を設定する(降順に並べるので加算) + if (firstSeenNormalPostIndex != 0) + { + var firstSeenSortKey = posts[firstSeenNormalPostIndex].CreatedAtForSorting; + foreach (var i in MyCommon.CountUp(0, firstSeenNormalPostIndex - 1)) + { + var distance = firstSeenNormalPostIndex - i; + posts[i] = posts[i] with + { + CreatedAtForSorting = firstSeenSortKey + TimeSpan.FromMilliseconds(distance), + }; + } + } + + // 最初に見つかった通常ツイートが末尾だった場合はここで終了 + if (firstSeenNormalPostIndex == posts.Length - 1) + return; + + // 通常ツイートと通常ツイートの間に挟まるプロモーションに対する処理 + var lastSeenNormalPostIndex = firstSeenNormalPostIndex; + foreach (var index in MyCommon.CountUp(firstSeenNormalPostIndex + 1, posts.Length - 1)) + { + var post = posts[index]; + if (post.IsPromoted) + continue; + + if (lastSeenNormalPostIndex != index - 1) + { + // lastSeenNormalPostIndex と index が隣接していない → 間にプロモーションがある + var lastNormalSortKey = posts[lastSeenNormalPostIndex].CreatedAtForSorting; + var nextNormalSortKey = post.CreatedAtForSorting; + var promotedPostCount = index - lastSeenNormalPostIndex - 1; + + // lastNormalSortKey と nextNormalSortKey の間の経過時間を均等に分割した時間をプロモーションのソートキーに設定する + var gap = lastNormalSortKey - nextNormalSortKey; + var durationPerPost = TimeSpan.FromTicks(gap.Ticks / (promotedPostCount + 1)); + var sortKey = lastNormalSortKey; + foreach (var promotedIndex in MyCommon.CountUp(lastSeenNormalPostIndex + 1, index - 1)) + { + sortKey -= durationPerPost; + posts[promotedIndex] = posts[promotedIndex] with + { + CreatedAtForSorting = sortKey, + }; + } + } + + lastSeenNormalPostIndex = index; + } + + // 最後に見つかった通常ツイートが末尾だった場合はここで終了 + if (lastSeenNormalPostIndex == posts.Length - 1) + return; + + // 最後の通常ツイートよりも後ろにプロモーションが挿入されていた場合は、 + // 最後の通常ツイートに対して 1ms ずつ減算した値を設定する(降順に並べるので減算) + var lastSeenSortKey = posts[lastSeenNormalPostIndex].CreatedAtForSorting; + foreach (var i in MyCommon.CountUp(lastSeenNormalPostIndex + 1, posts.Length - 1)) + { + var distance = i - lastSeenNormalPostIndex; + posts[i] = posts[i] with + { + CreatedAtForSorting = lastSeenSortKey - TimeSpan.FromMilliseconds(distance), + }; + } + } } } diff --git a/OpenTween/Models/UserTimelineTabModel.cs b/OpenTween/Models/UserTimelineTabModel.cs index c00081150..1d876ec0e 100644 --- a/OpenTween/Models/UserTimelineTabModel.cs +++ b/OpenTween/Models/UserTimelineTabModel.cs @@ -43,7 +43,7 @@ public override MyCommon.TabUsageType TabType public string ScreenName { get; } - public long OldestId { get; set; } = long.MaxValue; + public PostId? OldestId { get; set; } public UserTimelineTabModel(string tabName, string screenName) : base(tabName) diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index d8edf3430..e835b7e3d 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -588,7 +588,7 @@ public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, boo var request = new GetTimelineRequest(this.UserId) { MaxResults = count, - UntilId = more ? tab.OldestId.ToString() : null, + UntilId = more ? tab.OldestId as TwitterStatusId : null, }; var response = await request.Send(this.Api.Connection) @@ -604,15 +604,15 @@ public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, boo } else { - var maxId = more ? tab.OldestId : (long?)null; + var maxId = more ? tab.OldestId : null; - statuses = await this.Api.StatusesHomeTimeline(count, maxId) + statuses = await this.Api.StatusesHomeTimeline(count, maxId as TwitterStatusId) .ConfigureAwait(false); } var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read); if (minimumId != null) - tab.OldestId = minimumId.Value; + tab.OldestId = minimumId; } public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool more, bool startup) @@ -624,7 +624,7 @@ public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool m TwitterStatus[] statuses; if (more) { - statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId) + statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId as TwitterStatusId) .ConfigureAwait(false); } else @@ -635,7 +635,7 @@ public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool m var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Reply, tab, read); if (minimumId != null) - tab.OldestId = minimumId.Value; + tab.OldestId = minimumId; } public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTabModel tab, bool more) @@ -661,7 +661,7 @@ public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTab { if (more) { - statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId) + statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId as TwitterStatusId) .ConfigureAwait(false); } else @@ -674,7 +674,7 @@ public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTab var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read); if (minimumId != null) - tab.OldestId = minimumId.Value; + tab.OldestId = minimumId; } public async Task GetStatusApi(bool read, TwitterStatusId id) @@ -725,19 +725,26 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status) private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet); - private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read) + private PostId? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read) { - long? minimumId = null; + PostId? minimumId = null; - foreach (var status in items) + var posts = items.Select(x => this.CreatePostsFromStatusData(x)).ToArray(); + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); + + foreach (var post in posts) { - if (minimumId == null || minimumId.Value > status.Id) - minimumId = status.Id; + if (!post.IsPromoted) + { + if (minimumId == null || minimumId > post.StatusId) + minimumId = post.StatusId; + } // 二重取得回避 lock (this.lockObj) { - var id = new TwitterStatusId(status.IdStr); + var id = post.StatusId; if (tab == null) { if (TabInformations.GetInstance().ContainsKey(id)) continue; @@ -750,9 +757,7 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) // RT禁止ユーザーによるもの if (gType != MyCommon.WORKERTYPE.UserTimeline && - status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue; - - var post = this.CreatePostsFromStatusData(status); + post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue; post.IsRead = read; if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true; @@ -766,25 +771,32 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) return minimumId; } - private long? CreatePostsFromSearchJson(TwitterStatus[] statuses, PublicSearchTabModel tab, bool read, bool more) + private PostId? CreatePostsFromSearchJson(TwitterStatus[] statuses, PublicSearchTabModel tab, bool read, bool more) { - long? minimumId = null; + PostId? minimumId = null; + + var posts = statuses.Select(x => this.CreatePostsFromStatusData(x)).ToArray(); + + TwitterPostFactory.AdjustSortKeyForPromotedPost(posts); - foreach (var status in statuses) + foreach (var post in posts) { - if (minimumId == null || minimumId.Value > status.Id) - minimumId = status.Id; + if (!post.IsPromoted) + { + if (minimumId == null || minimumId > post.StatusId) + minimumId = post.StatusId; + + if (!more && (tab.SinceId == null || post.StatusId > tab.SinceId)) + tab.SinceId = post.StatusId; + } - if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id; // 二重取得回避 lock (this.lockObj) { - if (tab.Contains(new TwitterStatusId(status.IdStr))) + if (tab.Contains(post.StatusId)) continue; } - var post = this.CreatePostsFromStatusData(status); - post.IsRead = read; if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true; @@ -845,7 +857,7 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, } else if (more) { - statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts) + statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId as TwitterStatusId, includeRTs: SettingManager.Instance.Common.IsListsIncludeRts) .ConfigureAwait(false); } else @@ -857,7 +869,7 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read); if (minimumId != null) - tab.OldestId = minimumId.Value; + tab.OldestId = minimumId; } /// @@ -1049,15 +1061,15 @@ public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more) } else { - long? maxId = null; - long? sinceId = null; + TwitterStatusId? maxId = null; + TwitterStatusId? sinceId = null; if (more) { - maxId = tab.OldestId - 1; + maxId = tab.OldestId as TwitterStatusId; } else { - sinceId = tab.SinceId; + sinceId = tab.SinceId as TwitterStatusId; } var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId) @@ -1072,7 +1084,7 @@ public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more) var minimumId = this.CreatePostsFromSearchJson(statuses, tab, read, more); if (minimumId != null) - tab.OldestId = minimumId.Value; + tab.OldestId = minimumId; } public async Task GetDirectMessageEvents(bool read, DirectMessagesTabModel dmTab, bool backward)