From bbb4a20922e35799a699db1cb22ec6e1fb92acd6 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 16 Jun 2023 05:40:38 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=B3=20v3.5.1-dev=20=E9=96=8B=E7=99=BA=E9=96=8B=E5=A7=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Properties/AssemblyInfo.cs | 2 +- OpenTween/Properties/Resources.Designer.cs | 5 +++-- OpenTween/Resources/ChangeLog.txt | 2 ++ appveyor.yml | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/OpenTween/Properties/AssemblyInfo.cs b/OpenTween/Properties/AssemblyInfo.cs index a422fe16c..057ed19e9 100644 --- a/OpenTween/Properties/AssemblyInfo.cs +++ b/OpenTween/Properties/AssemblyInfo.cs @@ -22,7 +22,7 @@ // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")] -[assembly: AssemblyVersion("3.5.0.0")] +[assembly: AssemblyVersion("3.5.0.1")] [assembly: InternalsVisibleTo("OpenTween.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq diff --git a/OpenTween/Properties/Resources.Designer.cs b/OpenTween/Properties/Resources.Designer.cs index 4c2de913e..a7af59688 100644 --- a/OpenTween/Properties/Resources.Designer.cs +++ b/OpenTween/Properties/Resources.Designer.cs @@ -580,6 +580,8 @@ internal static string ChangeIconToolStripMenuItem_Confirm { /// /// 更新履歴 /// + ///==== Unreleased + /// ///==== Ver 3.5.0(2023/06/16) /// * CHG: アカウント追加時にOpenTweenのAPIキーによる認可を選択肢に追加 /// * CHG: 非対応のOSを使用している場合に起動時に警告を表示する @@ -593,8 +595,7 @@ internal static string ChangeIconToolStripMenuItem_Confirm { /// ///==== Ver 3.3.0(2023/01/22) /// * NEW: アカウント追加時にAPIキーを指定可能になりました - /// * CHG: API v2 の使用を設定状態に関わらず無効化しました - /// * FIX: 同 [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 + /// * CHG: API v2 の使用を設定状態に関わら [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 /// internal static string ChangeLog { get { diff --git a/OpenTween/Resources/ChangeLog.txt b/OpenTween/Resources/ChangeLog.txt index 53a1e6496..37af2be42 100644 --- a/OpenTween/Resources/ChangeLog.txt +++ b/OpenTween/Resources/ChangeLog.txt @@ -1,5 +1,7 @@ 更新履歴 +==== Unreleased + ==== Ver 3.5.0(2023/06/16) * CHG: アカウント追加時にOpenTweenのAPIキーによる認可を選択肢に追加 * CHG: 非対応のOSを使用している場合に起動時に警告を表示する diff --git a/appveyor.yml b/appveyor.yml index bf4b8edbb..572552955 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.4.0.{build} +version: 3.5.0.{build} os: Visual Studio 2022 From 7a7953a2f376bc558993bf539c1c64d8da34faa2 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 17 Jun 2023 07:41:15 +0900 Subject: [PATCH 02/21] =?UTF-8?q?TabInformations=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/TabInformationTest.cs | 191 +++++++++++++++++++ OpenTween/Models/TabInformations.cs | 2 + 2 files changed, 193 insertions(+) diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index 4545169f6..40e879327 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -24,6 +24,7 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Windows.Forms; using Xunit; using Xunit.Extensions; @@ -70,6 +71,102 @@ public void AddTab_DuplicateTest() Assert.False(ret); } + [Fact] + public void RemoveTab_InnerStorageTabTest() + { + var tab = new PublicSearchTabModel("search"); + tab.AddPostQueue(new PostClass { StatusId = 100L }); + this.tabinfo.AddTab(tab); + this.tabinfo.SubmitUpdate(); + + Assert.True(this.tabinfo.ContainsTab("search")); + Assert.Empty(this.tabinfo.RemovedTab); + + this.tabinfo.RemoveTab("search"); + + Assert.False(this.tabinfo.ContainsTab("search")); + Assert.Single(this.tabinfo.RemovedTab); + Assert.Contains(tab, this.tabinfo.RemovedTab); + } + + [Fact] + public void RemoveTab_FilterTab_MovedPost_OrphanedTest() + { + var filterTab = new FilterTabModel("filter"); + filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + Assert.False(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(filterTab.Contains(100L)); + + this.tabinfo.RemoveTab("filter"); + + Assert.False(this.tabinfo.ContainsTab("filter")); + Assert.Single(this.tabinfo.RemovedTab); + Assert.Contains(filterTab, this.tabinfo.RemovedTab); + + // 他に MoveMatches で移動している振り分けタブが存在しなければ Home タブに戻す + Assert.True(this.tabinfo.HomeTab.Contains(100L)); + } + + [Fact] + public void RemoveTab_FilterTab_MovedPost_NotOrphanedTest() + { + var filterTab1 = new FilterTabModel("filter1"); + filterTab1.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab1); + + var filterTab2 = new FilterTabModel("filter2"); + filterTab2.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab2); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + Assert.False(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(filterTab1.Contains(100L)); + Assert.True(filterTab2.Contains(100L)); + + this.tabinfo.RemoveTab("filter1"); + + Assert.False(this.tabinfo.ContainsTab("filter1")); + Assert.Single(this.tabinfo.RemovedTab); + Assert.Contains(filterTab1, this.tabinfo.RemovedTab); + + // 他に MoveMatches で移動している振り分けタブが存在する場合は Home タブに戻さない + Assert.False(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(filterTab2.Contains(100L)); + } + + [Fact] + public void RemoveTab_FilterTab_CopiedPost_Test() + { + var filterTab = new FilterTabModel("filter"); + filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = false }); + this.tabinfo.AddTab(filterTab); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + Assert.True(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(filterTab.Contains(100L)); + + this.tabinfo.RemoveTab("filter"); + + Assert.False(this.tabinfo.ContainsTab("filter")); + Assert.Single(this.tabinfo.RemovedTab); + Assert.Contains(filterTab, this.tabinfo.RemovedTab); + + // 振り分けタブにコピーされた発言は Home タブにも存在しているため何もしない + Assert.True(this.tabinfo.HomeTab.Contains(100L)); + } + [Fact] public void RenameTab_PositionTest() { @@ -95,6 +192,56 @@ public void RenameTab_SelectedTabTest() Assert.Equal(replyTab, this.tabinfo.SelectedTab); } + [Fact] + public void MoveTab_MoveToStart_Test() + { + Assert.Equal(0, this.tabinfo.Tabs.IndexOf("Recent")); + Assert.Equal(1, this.tabinfo.Tabs.IndexOf("Reply")); + + this.tabinfo.MoveTab(0, this.tabinfo.MentionTab); + + Assert.Equal(0, this.tabinfo.Tabs.IndexOf("Reply")); + Assert.Equal(1, this.tabinfo.Tabs.IndexOf("Recent")); + } + + [Fact] + public void MoveTab_MoveToEnd_Test() + { + Assert.Equal(4, this.tabinfo.Tabs.Count); + Assert.Equal(2, this.tabinfo.Tabs.IndexOf("DM")); + Assert.Equal(3, this.tabinfo.Tabs.IndexOf("Favorites")); + + this.tabinfo.MoveTab(3, this.tabinfo.DirectMessageTab); + + Assert.Equal(2, this.tabinfo.Tabs.IndexOf("Favorites")); + Assert.Equal(3, this.tabinfo.Tabs.IndexOf("DM")); + } + + [Fact] + public void MoveTab_OutOfRangeError_Test() + { + Assert.Equal(4, this.tabinfo.Tabs.Count); + Assert.Throws( + () => this.tabinfo.MoveTab(-1, this.tabinfo.HomeTab) + ); + Assert.Throws( + () => this.tabinfo.MoveTab(4, this.tabinfo.HomeTab) + ); + } + + [Theory] + [InlineData("Reply", true)] + [InlineData("UNKNOWN NAME", false)] + public void ContainsTab_TabName_Test(string tabName, bool expected) + => Assert.Equal(expected, this.tabinfo.ContainsTab(tabName)); + + [Fact] + public void ContainsTab_TabInstance_Test() + { + Assert.True(this.tabinfo.ContainsTab(this.tabinfo.HomeTab)); + Assert.False(this.tabinfo.ContainsTab(new PublicSearchTabModel("tab"))); + } + [Fact] public void SelectTab_Test() { @@ -226,6 +373,50 @@ public void MakeTabName_RetryErrorTest() Assert.Throws(() => this.tabinfo.MakeTabName(baseTabName, 5)); } + [Fact] + public void SetSortMode_Test() + { + this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending); + Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder); + Assert.Equal(ComparerMode.Id, this.tabinfo.HomeTab.SortMode); + Assert.Equal(SortOrder.Descending, this.tabinfo.HomeTab.SortOrder); + + this.tabinfo.SetSortMode(ComparerMode.Source, SortOrder.Ascending); + Assert.Equal(ComparerMode.Source, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder); + Assert.Equal(ComparerMode.Source, this.tabinfo.HomeTab.SortMode); + Assert.Equal(SortOrder.Ascending, this.tabinfo.HomeTab.SortOrder); + } + + [Fact] + public void ToggleSortOrder_SameMode_Test() + { + this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending); + Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder); + + this.tabinfo.ToggleSortOrder(ComparerMode.Id); + Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder); + + this.tabinfo.ToggleSortOrder(ComparerMode.Id); + Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder); + } + + [Fact] + public void ToggleSortOrder_OtherMode_Test() + { + this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending); + Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder); + + this.tabinfo.ToggleSortOrder(ComparerMode.Source); + Assert.Equal(ComparerMode.Source, this.tabinfo.SortMode); + Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder); + } + [Fact] public void IsMuted_Test() { diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index 9ef2b85b4..dbb125e4b 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -178,6 +178,8 @@ public void RemoveTab(string tabName) public void MoveTab(int newIndex, TabModel tab) { + if (newIndex < 0 || newIndex >= this.tabs.Count) + throw new ArgumentOutOfRangeException(nameof(newIndex)); if (!this.ContainsTab(tab)) throw new ArgumentOutOfRangeException(nameof(tab)); From d64811b414a4c50a68f65a8227d81eda7fb785ec Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 17 Jun 2023 08:44:13 +0900 Subject: [PATCH 03/21] =?UTF-8?q?=E3=82=BF=E3=83=96=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=AE=E5=8F=96=E6=B6=88=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?TabInformations=E3=81=AB=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/TabInformationTest.cs | 55 ++++++++++++++++++++ OpenTween/Models/TabInformations.cs | 21 ++++++++ OpenTween/Tween.cs | 33 +++--------- 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index 40e879327..b85953406 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -167,6 +167,61 @@ public void RemoveTab_FilterTab_CopiedPost_Test() Assert.True(this.tabinfo.HomeTab.Contains(100L)); } + [Fact] + public void CanUndoRemovedTab_Test() + { + var tab = new PublicSearchTabModel("tab"); + this.tabinfo.AddTab(tab); + Assert.False(this.tabinfo.CanUndoRemovedTab); + + this.tabinfo.RemoveTab(tab.TabName); + Assert.True(this.tabinfo.CanUndoRemovedTab); + } + + [Fact] + public void UndoRemovedTab_Test() + { + var tab = new PublicSearchTabModel("tab"); + this.tabinfo.AddTab(tab); + Assert.True(this.tabinfo.ContainsTab("tab")); + Assert.Empty(this.tabinfo.RemovedTab); + + this.tabinfo.RemoveTab("tab"); + Assert.False(this.tabinfo.ContainsTab("tab")); + Assert.Single(this.tabinfo.RemovedTab); + + var restoredTab = this.tabinfo.UndoRemovedTab(); + Assert.True(this.tabinfo.ContainsTab("tab")); + Assert.Same(tab, restoredTab); + Assert.Empty(this.tabinfo.RemovedTab); + } + + [Fact] + public void UndoRemovedTab_EmptyError_Test() + { + Assert.Empty(this.tabinfo.RemovedTab); + Assert.Throws( + () => this.tabinfo.UndoRemovedTab() + ); + } + + [Fact] + public void UndoRemovedTab_DuplicatedName_Test() + { + var tab = new PublicSearchTabModel("tab"); + this.tabinfo.AddTab(tab); + Assert.Empty(this.tabinfo.RemovedTab); + + this.tabinfo.RemoveTab("tab"); + Assert.Single(this.tabinfo.RemovedTab); + + this.tabinfo.RenameTab("Recent", "tab"); + Assert.Throws( + () => this.tabinfo.UndoRemovedTab() + ); + Assert.Single(this.tabinfo.RemovedTab); + } + [Fact] public void RenameTab_PositionTest() { diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index dbb125e4b..1941ab8dd 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -176,6 +176,27 @@ public void RemoveTab(string tabName) } } + public bool CanUndoRemovedTab + => this.RemovedTab.Count > 0; + + public TabModel UndoRemovedTab() + { + if (!this.CanUndoRemovedTab) + throw new TabException("There isn't removed tab."); + + var tab = this.RemovedTab.Pop(); + if (this.ContainsTab(tab.TabName)) + { + this.RemovedTab.Push(tab); + var message = string.Format(Properties.Resources.UndoRemovedTab_DuplicateError, tab.TabName); + throw new TabException(message); + } + + this.AddTab(tab); + + return tab; + } + public void MoveTab(int newIndex, TabModel tab) { if (newIndex < 0 || newIndex >= this.tabs.Count) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 9ce577427..1e5bb4756 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -8691,30 +8691,20 @@ public int GetTabPageIndex(string tabName) private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e) { - if (this.statuses.RemovedTab.Count == 0) - { - MessageBox.Show("There isn't removed tab.", "Undo", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - else + try { - var tb = this.statuses.RemovedTab.Pop(); - if (this.statuses.ContainsTab(tb.TabName)) - { - var message = string.Format(Properties.Resources.UndoRemovedTab_DuplicateError, tb.TabName); - MessageBox.Show(this, message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error); - this.statuses.RemovedTab.Push(tb); - return; - } - - this.statuses.AddTab(tb); - this.AddNewTab(tb, startup: false); + var restoredTab = this.statuses.UndoRemovedTab(); + this.AddNewTab(restoredTab, startup: false); var tabIndex = this.statuses.Tabs.Count - 1; this.ListTab.SelectedIndex = tabIndex; this.SaveConfigsTabs(); } + catch (TabException ex) + { + MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error); + } } private async Task DoMoveToRTHome() @@ -8963,14 +8953,7 @@ private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e) private void MenuItemEdit_DropDownOpening(object sender, EventArgs e) { - if (this.statuses.RemovedTab.Count == 0) - { - this.UndoRemoveTabMenuItem.Enabled = false; - } - else - { - this.UndoRemoveTabMenuItem.Enabled = true; - } + this.UndoRemoveTabMenuItem.Enabled = this.statuses.CanUndoRemovedTab; if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch) this.PublicSearchQueryMenuItem.Enabled = true; From ee9245ca6c84b57eb1ccbbd8e6d4c84bf095ea67 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sat, 17 Jun 2023 19:34:06 +0900 Subject: [PATCH 04/21] =?UTF-8?q?OpenTween.Tests=E3=83=97=E3=83=AD?= =?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AE=E4=BE=9D=E5=AD=98?= =?UTF-8?q?=E9=96=A2=E4=BF=82=E3=82=92=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 4 ++-- OpenTween.Tests/OpenTween.Tests.csproj | 10 +++++----- appveyor.yml | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5ff3445c2..58fb98cac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,8 +83,8 @@ jobs: - name: Run tests shell: pwsh run: | - $altCoverVersion = '8.2.837' - $xunitVersion = '2.4.1' + $altCoverVersion = '8.6.61' + $xunitVersion = '2.4.2' $targetFramework = 'net48' $altCoverPath = "$($env:NUGET_PACKAGES)\altcover\$($altCoverVersion)\tools\net472\AltCover.exe" $xunitPath = "$($env:NUGET_PACKAGES)\xunit.runner.console\$($xunitVersion)\tools\net472\xunit.console.exe" diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index 25147b7e3..1b99bd52a 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -25,18 +25,18 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/appveyor.yml b/appveyor.yml index 572552955..72bd68aac 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -64,8 +64,8 @@ before_build: test_script: - cmd: | - set altCoverVersion=8.2.837 - set xunitVersion=2.4.1 + set altCoverVersion=8.6.61 + set xunitVersion=2.4.2 set targetFramework=net48 set nugetPackages=%UserProfile%\.nuget\packages From c73424d07dace3d05aaa605b241d191b48350ef5 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 10:47:22 +0900 Subject: [PATCH 05/21] =?UTF-8?q?TabInformations.ClearTabIds=E3=81=A7?= =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=9F=E7=99=BA?= =?UTF-8?q?=E8=A8=80=E3=81=8CPosts=E3=81=8B=E3=82=89=E5=89=8A=E9=99=A4?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=A0=B4=E5=90=88=E3=81=8C?= =?UTF-8?q?=E3=81=82=E3=82=8B=E4=B8=8D=E5=85=B7=E5=90=88=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/TabInformationTest.cs | 80 ++++++++++++++++++++ OpenTween/Models/TabInformations.cs | 6 ++ OpenTween/Resources/ChangeLog.txt | 1 + 3 files changed, 87 insertions(+) diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index b85953406..bc80d4e3d 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -1137,6 +1137,86 @@ public void FilterAll_ExcludeReplyFilterTest() Assert.True(this.tabinfo[200L]!.IsExcludeReply); } + [Fact] + public void ClearTabIds_InnerStorageTabTest() + { + var tab = new PublicSearchTabModel("search"); + tab.AddPostQueue(new PostClass { StatusId = 100L }); + this.tabinfo.AddTab(tab); + this.tabinfo.SubmitUpdate(); + + Assert.True(tab.Contains(100L)); + + this.tabinfo.ClearTabIds("search"); + Assert.False(tab.Contains(100L)); + } + + [Fact] + public void ClearTabIds_FilterTab_MovedPost_OrphanedTest() + { + var filterTab = new FilterTabModel("filter"); + filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + Assert.True(this.tabinfo.Posts.ContainsKey(100L)); + Assert.True(filterTab.Contains(100L)); + + this.tabinfo.ClearTabIds("filter"); + Assert.False(this.tabinfo.Posts.ContainsKey(100L)); + Assert.False(filterTab.Contains(100L)); + } + + [Fact] + public void ClearTabIds_FilterTab_MovedPost_NotOrphanedTest() + { + var filterTab1 = new FilterTabModel("filter1"); + filterTab1.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab1); + + var filterTab2 = new FilterTabModel("filter2"); + filterTab2.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); + this.tabinfo.AddTab(filterTab2); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + Assert.True(this.tabinfo.Posts.ContainsKey(100L)); + Assert.True(filterTab1.Contains(100L)); + Assert.True(filterTab2.Contains(100L)); + + this.tabinfo.ClearTabIds("filter1"); + + // 他に MoveMatches で移動している振り分けタブが存在する場合は TabInformations.Posts から削除しない + Assert.True(this.tabinfo.ContainsKey(100L)); + Assert.False(filterTab1.Contains(100L)); + Assert.True(filterTab2.Contains(100L)); + } + + [Fact] + public void ClearTabIds_NotAffectToOtherTabs_Test() + { + var otherTab = new PublicSearchTabModel("search"); + otherTab.AddPostQueue(new PostClass { StatusId = 100L }); + this.tabinfo.AddTab(otherTab); + + this.tabinfo.AddPost(new PostClass { StatusId = 100L }); + this.tabinfo.DistributePosts(); + this.tabinfo.SubmitUpdate(); + + // Recent, search のタブに status_id = 100 の発言が存在する状態 + Assert.True(this.tabinfo.Posts.ContainsKey(100L)); + Assert.True(otherTab.Contains(100L)); + + this.tabinfo.ClearTabIds("Recent"); + Assert.False(this.tabinfo.Posts.ContainsKey(100L)); + Assert.True(otherTab.Contains(100L)); + } + [Fact] public void RefreshOwl_HomeTabTest() { diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index 1941ab8dd..e606710be 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -864,6 +864,12 @@ public void ClearTabIds(string tabName) var hit = false; foreach (var tab in this.Tabs) { + if (tab is InternalStorageTabModel) + continue; + + if (tab == tb) + continue; + if (tab.Contains(id)) { hit = true; diff --git a/OpenTween/Resources/ChangeLog.txt b/OpenTween/Resources/ChangeLog.txt index 37af2be42..0bcd68584 100644 --- a/OpenTween/Resources/ChangeLog.txt +++ b/OpenTween/Resources/ChangeLog.txt @@ -1,6 +1,7 @@ 更新履歴 ==== Unreleased + * FIX: 「このタブの発言をクリア」で参照されなくなった発言分のメモリが開放されない場合がある不具合を修正 ==== Ver 3.5.0(2023/06/16) * CHG: アカウント追加時にOpenTweenのAPIキーによる認可を選択肢に追加 From 505fcf2d6711ce5108a0a7a8aed8e8e43cab79e1 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 15:13:36 +0900 Subject: [PATCH 06/21] =?UTF-8?q?PostClass.IsFav=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=99=82=E3=81=ABRT=E5=85=83=E3=81=AEPostClass=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B=E6=8C=99=E5=8B=95=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/PostClassTest.cs | 104 ++++-------------------- OpenTween/Models/PostClass.cs | 35 +------- OpenTween/Models/TabInformations.cs | 3 - 3 files changed, 19 insertions(+), 123 deletions(-) diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index eebcf63e4..190e24194 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -32,51 +32,6 @@ namespace OpenTween.Models { public class PostClassTest { - private class PostClassGroup - { - private readonly Dictionary testCases; - - public PostClassGroup(params TestPostClass[] postClasses) - { - this.testCases = new Dictionary(); - foreach (var p in postClasses) - { - p.Group = this; - this.testCases.Add(p.StatusId, p); - } - } - - public PostClass this[long id] => this.testCases[id]; - } - - private class TestPostClass : PostClass - { - public PostClassGroup? Group; - - protected override PostClass RetweetSource - { - get - { - var retweetedId = this.RetweetedId!.Value; - var group = this.Group; - if (group == null) - throw new InvalidOperationException("TestPostClass needs group"); - - return group[retweetedId]; - } - } - } - - private readonly PostClassGroup postGroup; - - public PostClassTest() - { - this.postGroup = new PostClassGroup( - new TestPostClass { StatusId = 1L }, - new TestPostClass { StatusId = 2L, IsFav = true }, - new TestPostClass { StatusId = 3L, IsFav = false, RetweetedId = 2L }); - } - [Fact] public void CloneTest() { @@ -96,29 +51,6 @@ public void TextSingleLineTest(string text, string expected) Assert.Equal(expected, post.TextSingleLine); } - [Theory] - [InlineData(1L, false)] - [InlineData(2L, true)] - [InlineData(3L, true)] - public void GetIsFavTest(long statusId, bool expected) - => Assert.Equal(expected, this.postGroup[statusId].IsFav); - - [Theory] - [InlineData(2L, true)] - [InlineData(2L, false)] - [InlineData(3L, true)] - [InlineData(3L, false)] - public void SetIsFavTest(long statusId, bool isFav) - { - var post = this.postGroup[statusId]; - - post.IsFav = isFav; - Assert.Equal(isFav, post.IsFav); - - if (post.RetweetedId != null) - Assert.Equal(isFav, this.postGroup[post.RetweetedId.Value].IsFav); - } - #pragma warning disable SA1008 // Opening parenthesis should be spaced correctly [Theory] [InlineData(false, false, false, false, -0x01)] @@ -140,7 +72,7 @@ public void SetIsFavTest(long statusId, bool isFav) #pragma warning restore SA1008 public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int expected) { - var post = new TestPostClass + var post = new PostClass { IsProtect = protect, IsMark = mark, @@ -154,7 +86,7 @@ public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int ex [Fact] public void SourceHtml_Test() { - var post = new TestPostClass + var post = new PostClass { Source = "Twitter Web Client", SourceUri = new Uri("http://twitter.com/"), @@ -166,7 +98,7 @@ public void SourceHtml_Test() [Fact] public void SourceHtml_PlainTextTest() { - var post = new TestPostClass + var post = new PostClass { Source = "web", SourceUri = null, @@ -178,7 +110,7 @@ public void SourceHtml_PlainTextTest() [Fact] public void SourceHtml_EscapeTest() { - var post = new TestPostClass + var post = new PostClass { Source = "", SourceUri = new Uri("http://example.com/?aaa=123&bbb=456"), @@ -190,7 +122,7 @@ public void SourceHtml_EscapeTest() [Fact] public void SourceHtml_EscapePlainTextTest() { - var post = new TestPostClass + var post = new PostClass { Source = "", SourceUri = null, @@ -202,7 +134,7 @@ public void SourceHtml_EscapePlainTextTest() [Fact] public void DeleteTest() { - var post = new TestPostClass + var post = new PostClass { InReplyToStatusId = 10L, InReplyToUser = "hogehoge", @@ -224,7 +156,7 @@ public void DeleteTest() [Fact] public void CanDeleteBy_SentDMTest() { - var post = new TestPostClass + var post = new PostClass { IsDm = true, IsMe = true, // 自分が送信した DM @@ -237,7 +169,7 @@ public void CanDeleteBy_SentDMTest() [Fact] public void CanDeleteBy_ReceivedDMTest() { - var post = new TestPostClass + var post = new PostClass { IsDm = true, IsMe = false, // 自分が受け取った DM @@ -250,7 +182,7 @@ public void CanDeleteBy_ReceivedDMTest() [Fact] public void CanDeleteBy_MyTweetTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 111L, // 自分のツイート }; @@ -261,7 +193,7 @@ public void CanDeleteBy_MyTweetTest() [Fact] public void CanDeleteBy_OthersTweetTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 222L, // 他人のツイート }; @@ -272,7 +204,7 @@ public void CanDeleteBy_OthersTweetTest() [Fact] public void CanDeleteBy_RetweetedByMeTest() { - var post = new TestPostClass + var post = new PostClass { RetweetedByUserId = 111L, // 自分がリツイートした UserId = 222L, // 他人のツイート @@ -284,7 +216,7 @@ public void CanDeleteBy_RetweetedByMeTest() [Fact] public void CanDeleteBy_RetweetedByOthersTest() { - var post = new TestPostClass + var post = new PostClass { RetweetedByUserId = 333L, // 他人がリツイートした UserId = 222L, // 他人のツイート @@ -296,7 +228,7 @@ public void CanDeleteBy_RetweetedByOthersTest() [Fact] public void CanDeleteBy_MyTweetHaveBeenRetweetedByOthersTest() { - var post = new TestPostClass + var post = new PostClass { RetweetedByUserId = 222L, // 他人がリツイートした UserId = 111L, // 自分のツイート @@ -308,7 +240,7 @@ public void CanDeleteBy_MyTweetHaveBeenRetweetedByOthersTest() [Fact] public void CanRetweetBy_DMTest() { - var post = new TestPostClass + var post = new PostClass { IsDm = true, IsMe = false, // 自分が受け取った DM @@ -321,7 +253,7 @@ public void CanRetweetBy_DMTest() [Fact] public void CanRetweetBy_MyTweetTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 111L, // 自分のツイート }; @@ -332,7 +264,7 @@ public void CanRetweetBy_MyTweetTest() [Fact] public void CanRetweetBy_ProtectedMyTweetTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 111L, // 自分のツイート IsProtect = true, @@ -344,7 +276,7 @@ public void CanRetweetBy_ProtectedMyTweetTest() [Fact] public void CanRetweetBy_OthersTweet_NotProtectedTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 222L, // 他人のツイート IsProtect = false, @@ -356,7 +288,7 @@ public void CanRetweetBy_OthersTweet_NotProtectedTest() [Fact] public void CanRetweetBy_OthersTweet_ProtectedTest() { - var post = new TestPostClass + var post = new PostClass { UserId = 222L, // 他人のツイート IsProtect = true, diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index e52e333f7..d2d2c78f3 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -60,8 +60,6 @@ double Latitude public long StatusId { get; set; } - private bool isFav; - public string Text { get @@ -211,35 +209,7 @@ public PostClass() public string TextSingleLine => this.TextFromApi.Replace("\n", " "); - public bool IsFav - { - get - { - if (this.RetweetedId != null) - { - var post = this.RetweetSource; - if (post != null) - { - return post.IsFav; - } - } - - return this.isFav; - } - - set - { - this.isFav = value; - if (this.RetweetedId != null) - { - var post = this.RetweetSource; - if (post != null) - { - post.IsFav = value; - } - } - } - } + public bool IsFav { get; set; } public bool IsProtect { @@ -301,9 +271,6 @@ public bool IsDeleted } } - protected virtual PostClass? RetweetSource - => this.RetweetedId != null ? TabInformations.GetInstance().RetweetSource(this.RetweetedId.Value) : null; - public StatusGeo? PostGeo { get => this.postGeo; diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index e606710be..b41312a10 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -383,9 +383,6 @@ public SortOrder ToggleSortOrder(ComparerMode sortMode) return this.SortOrder; } - public PostClass? RetweetSource(long id) - => this.Posts.TryGetValue(id, out var status) ? status : null; - public void RemovePostFromAllTabs(long statusId, bool setIsDeleted) { foreach (var tab in this.Tabs) From ae995098b68eff9259a1ecee40a510b43b4ca3d2 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 15:21:26 +0900 Subject: [PATCH 07/21] =?UTF-8?q?PostClass.StateIndex=E3=82=92getter?= =?UTF-8?q?=E5=86=85=E3=81=A7=E9=83=BD=E5=BA=A6=E8=A8=88=E7=AE=97=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Models/PostClass.cs | 81 ++++++++++------------------------- 1 file changed, 22 insertions(+), 59 deletions(-) diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index d2d2c78f3..56709d023 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -185,7 +185,6 @@ object ICloneable.Clone() public int FavoritedCount { get; set; } - private States states = States.None; private bool expandComplatedAll = false; [Flags] @@ -206,52 +205,35 @@ public PostClass() this.ExpandedUrls = Array.Empty(); } - public string TextSingleLine - => this.TextFromApi.Replace("\n", " "); - - public bool IsFav { get; set; } - - public bool IsProtect + public int StateIndex { - get => this.isProtect; - set + get { - if (value) - this.states |= States.Protect; - else - this.states &= ~States.Protect; - - this.isProtect = value; + var states = States.None; + + if (this.IsProtect) + states |= States.Protect; + if (this.IsMark) + states |= States.Mark; + if (this.InReplyToStatusId != null) + states |= States.Reply; + if (this.PostGeo != null) + states |= States.Geo; + + return (int)states - 1; } } - public bool IsMark - { - get => this.isMark; - set - { - if (value) - this.states |= States.Mark; - else - this.states &= ~States.Mark; + public string TextSingleLine + => this.TextFromApi.Replace("\n", " "); - this.isMark = value; - } - } + public bool IsFav { get; set; } - public long? InReplyToStatusId - { - get => this.inReplyToStatusId; - set - { - if (value != null) - this.states |= States.Reply; - else - this.states &= ~States.Reply; + public bool IsProtect { get; set; } - this.inReplyToStatusId = value; - } - } + public bool IsMark { get; set; } + + public long? InReplyToStatusId { get; set; } public bool IsDeleted { @@ -265,31 +247,12 @@ public bool IsDeleted this.InReplyToUserId = null; this.IsReply = false; this.ReplyToList = new List<(long, string)>(); - this.states = States.None; } this.isDeleted = value; } } - public StatusGeo? PostGeo - { - get => this.postGeo; - set - { - if (value != null) - { - this.states |= States.Geo; - } - else - { - this.states &= ~States.Geo; - } - this.postGeo = value; - } - } - - public int StateIndex - => (int)this.states - 1; + public StatusGeo? PostGeo { get; set; } // 互換性のために用意 public string SourceHtml From f28df207c4872484270bc6a2016c2a1fb86e4d7d Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 15:22:42 +0900 Subject: [PATCH 08/21] =?UTF-8?q?PostClass.IsDeleted=20=3D=20true=20?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E6=99=82=E3=81=AB=E4=BB=96=E3=81=AE=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=82=92=E5=8A=A0=E3=81=88=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/PostClassTest.cs | 22 ---------------------- OpenTween/Models/PostClass.cs | 17 +---------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 190e24194..7c0d0fd64 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -131,28 +131,6 @@ public void SourceHtml_EscapePlainTextTest() Assert.Equal("<script>alert(1)</script>", post.SourceHtml); } - [Fact] - public void DeleteTest() - { - var post = new PostClass - { - InReplyToStatusId = 10L, - InReplyToUser = "hogehoge", - InReplyToUserId = 100L, - IsReply = true, - ReplyToList = { (100L, "hogehoge") }, - }; - - post.IsDeleted = true; - - Assert.Null(post.InReplyToStatusId); - Assert.Equal("", post.InReplyToUser); - Assert.Null(post.InReplyToUserId); - Assert.False(post.IsReply); - Assert.Empty(post.ReplyToList); - Assert.Equal(-1, post.StateIndex); - } - [Fact] public void CanDeleteBy_SentDMTest() { diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 56709d023..3651a976e 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -235,22 +235,7 @@ public string TextSingleLine public long? InReplyToStatusId { get; set; } - public bool IsDeleted - { - get => this.isDeleted; - set - { - if (value) - { - this.InReplyToStatusId = null; - this.InReplyToUser = ""; - this.InReplyToUserId = null; - this.IsReply = false; - this.ReplyToList = new List<(long, string)>(); - } - this.isDeleted = value; - } - } + public bool IsDeleted { get; set; } public StatusGeo? PostGeo { get; set; } From 0d8496bce7664e17291fd2768def86c679bd63b6 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 15:28:16 +0900 Subject: [PATCH 09/21] =?UTF-8?q?TwitterPostFactory=E3=81=A7=E3=81=AEPostC?= =?UTF-8?q?lass=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=BF=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E7=94=9F=E6=88=90=E3=81=AB=E3=82=AA=E3=83=96=E3=82=B8?= =?UTF-8?q?=E3=82=A7=E3=82=AF=E3=83=88=E5=88=9D=E6=9C=9F=E5=8C=96=E5=AD=90?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Api/DataModel/TwitterUser.cs | 12 + OpenTween/Models/TwitterPostFactory.cs | 375 +++++++++++-------------- 2 files changed, 173 insertions(+), 214 deletions(-) diff --git a/OpenTween/Api/DataModel/TwitterUser.cs b/OpenTween/Api/DataModel/TwitterUser.cs index dfb9a4550..a1597d05d 100644 --- a/OpenTween/Api/DataModel/TwitterUser.cs +++ b/OpenTween/Api/DataModel/TwitterUser.cs @@ -115,5 +115,17 @@ public class TwitterUserEntity /// public static TwitterUser ParseJson(string json) => MyCommon.CreateDataFromJson(json); + + public static TwitterUser CreateUnknownUser() + { + return new() + { + Id = 0L, + IdStr = "0", + ScreenName = "?????", + Name = "Unknown User", + ProfileImageUrlHttps = "", + }; + } } } diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index b792b4bda..c29cbbc82 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -58,174 +58,120 @@ public PostClass CreateFromStatus( bool favTweet = false ) { - var post = new PostClass(); - TwitterEntities entities; - string sourceHtml; + var statusUser = status.User ?? TwitterUser.CreateUnknownUser(); - post.StatusId = status.Id; + // リツイートでない場合は null + var retweetedStatus = (TwitterStatus?)null; + var retweeterUser = (TwitterUser?)null; if (status.RetweetedStatus != null) { - var retweeted = status.RetweetedStatus; - - post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt); - - // Id - post.RetweetedId = retweeted.Id; - // 本文 - post.TextFromApi = retweeted.FullText; - entities = retweeted.MergedEntities; - sourceHtml = retweeted.Source; - // Reply先 - post.InReplyToStatusId = retweeted.InReplyToStatusId; - post.InReplyToUser = retweeted.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; - - if (favTweet) - { - post.IsFav = true; - } - else - { - // 幻覚fav対策 - var favTab = this.tabinfo.FavoriteTab; - post.IsFav = favTab.Contains(retweeted.Id); - } - - if (retweeted.Coordinates != null) - post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]); - - // 以下、ユーザー情報 - var user = retweeted.User; - if (user != null) - { - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - } - else - { - post.UserId = 0L; - post.ScreenName = "?????"; - post.Nickname = "Unknown User"; - } - - // Retweetした人 - if (status.User != null) - { - post.RetweetedBy = status.User.ScreenName; - post.RetweetedByUserId = status.User.Id; - post.IsMe = post.RetweetedByUserId == selfUserId; - } - else - { - post.RetweetedBy = "?????"; - post.RetweetedByUserId = 0L; - } + // リツイート元のツイート + retweetedStatus = status.RetweetedStatus; + // リツイートを行ったユーザー + retweeterUser = statusUser; } - else - { - post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt); - // 本文 - post.TextFromApi = status.FullText; - entities = status.MergedEntities; - sourceHtml = status.Source; - post.InReplyToStatusId = status.InReplyToStatusId; - post.InReplyToUser = status.InReplyToScreenName; - post.InReplyToUserId = status.InReplyToUserId; - - if (favTweet) - { - post.IsFav = true; - } - else - { - // 幻覚fav対策 - var favTab = this.tabinfo.FavoriteTab; - post.IsFav = favTab.Posts.TryGetValue(post.StatusId, out var tabinfoPost) && tabinfoPost.IsFav; - } - if (status.Coordinates != null) - post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]); + // リツイートであるか否かに関わらず常にオリジナルのツイート及びユーザーを指す + var originalStatus = retweetedStatus ?? status; + var originalStatusUser = originalStatus.User ?? TwitterUser.CreateUnknownUser(); - // 以下、ユーザー情報 - var user = status.User; - if (user != null) - { - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - post.IsMe = post.UserId == selfUserId; - } - else - { - post.UserId = 0L; - post.ScreenName = "?????"; - post.Nickname = "Unknown User"; - } + bool isFav = favTweet; + if (isFav == false) + { + // 幻覚fav対策 (8a5717dd のコミット参照) + var favTab = this.tabinfo.FavoriteTab; + isFav = favTab.Contains(originalStatus.Id); } - // HTMLに整形 - var textFromApi = post.TextFromApi; - var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink; + var geo = (PostClass.StatusGeo?)null; + if (originalStatus.Coordinates != null) + geo = new(originalStatus.Coordinates.Coordinates[0], originalStatus.Coordinates.Coordinates[1]); + + var entities = originalStatus.MergedEntities; + var quotedStatusLink = originalStatus.QuotedStatusPermalink; if (quotedStatusLink != null && entities.Urls != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded)) quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある - post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink); - post.TextFromApi = textFromApi; - post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink); - post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); - post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); - post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink); - post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); - post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); + // HTMLに整形 + var text = CreateHtmlAnchor(originalStatus.FullText, entities, quotedStatusLink); + + var textFromApi = this.ReplaceTextFromApi(originalStatus.FullText, entities, quotedStatusLink); + textFromApi = WebUtility.HtmlDecode(textFromApi); + textFromApi = textFromApi.Replace("<3", "\u2661"); + + var accessibleText = CreateAccessibleText(originalStatus.FullText, entities, originalStatus.QuotedStatus, quotedStatusLink); + accessibleText = WebUtility.HtmlDecode(accessibleText); + accessibleText = accessibleText.Replace("<3", "\u2661"); - this.ExtractEntities(entities, post.ReplyToList, post.Media); + var (replyToList, media) = this.ExtractEntities(entities); - post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) - .Where(x => x != post.StatusId && x != post.RetweetedId) + var quoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) + .Where(x => x != status.Id && x != originalStatus.Id) .Distinct().ToArray(); - post.ExpandedUrls = entities.OfType() + var expandedUrls = entities.OfType() .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) .ToArray(); // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) - if (post.Text == post.TextFromApi) - post.Text = post.TextFromApi; - if (post.AccessibleText == post.TextFromApi) - post.AccessibleText = post.TextFromApi; + if (text == textFromApi) + text = textFromApi; + + if (accessibleText == textFromApi) + accessibleText = textFromApi; // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す - post.ScreenName = string.Intern(post.ScreenName); - post.Nickname = string.Intern(post.Nickname); - post.ImageUrl = string.Intern(post.ImageUrl); - post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null; + var screenName = string.Intern(originalStatusUser.ScreenName); + var nickname = string.Intern(originalStatusUser.Name); + var imageUrl = string.Intern(originalStatusUser.ProfileImageUrlHttps); // Source整形 - var (sourceText, sourceUri) = ParseSource(sourceHtml); - post.Source = string.Intern(sourceText); - post.SourceUri = sourceUri; + var (sourceText, sourceUri) = ParseSource(originalStatus.Source); - post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.UserId == selfUserId); - post.IsExcludeReply = false; + var isOwl = false; + if (followerIds.Count > 0) + isOwl = !followerIds.Contains(originalStatusUser.Id); - if (post.IsMe) + return new() { - post.IsOwl = false; - } - else - { - if (followerIds.Count > 0) - post.IsOwl = !followerIds.Contains(post.UserId); - } - - post.IsDm = false; - return post; + // status から生成 + StatusId = status.Id, + IsMe = statusUser.Id == selfUserId, + + // originalStatus から生成 + CreatedAt = MyCommon.DateTimeParse(originalStatus.CreatedAt), + Text = text, + TextFromApi = textFromApi, + AccessibleText = accessibleText, + QuoteStatusIds = quoteStatusIds, + ExpandedUrls = expandedUrls, + ReplyToList = replyToList, + Media = media, + PostGeo = geo, + Source = string.Intern(sourceText), + SourceUri = sourceUri, + IsFav = isFav, + IsReply = retweetedStatus != null && replyToList.Any(x => x.UserId == selfUserId), + InReplyToStatusId = originalStatus.InReplyToStatusId, + InReplyToUser = originalStatus.InReplyToScreenName, + InReplyToUserId = originalStatus.InReplyToUserId, + + // originalStatusUser から生成 + UserId = originalStatusUser.Id, + ScreenName = screenName, + Nickname = nickname, + ImageUrl = imageUrl, + IsProtect = originalStatusUser.Protected, + IsOwl = isOwl, + + // retweetedStatus から生成 + RetweetedId = retweetedStatus?.Id, + + // retweeterUser から生成 + RetweetedBy = retweeterUser != null ? string.Intern(retweeterUser.ScreenName) : null, + RetweetedByUserId = retweeterUser?.Id, + }; } public PostClass CreateFromDirectMessageEvent( @@ -235,13 +181,11 @@ public PostClass CreateFromDirectMessageEvent( long selfUserId ) { - var post = new PostClass(); - post.StatusId = long.Parse(eventItem.Id); - var timestamp = long.Parse(eventItem.CreatedTimestamp); - post.CreatedAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond); + var createdAt = DateTimeUtc.UnixEpoch + TimeSpan.FromTicks(timestamp * TimeSpan.TicksPerMillisecond); + // 本文 - var textFromApi = eventItem.MessageCreate.MessageData.Text; + var rawText = eventItem.MessageCreate.MessageData.Text; var entities = eventItem.MessageCreate.MessageData.Entities; var mediaEntity = eventItem.MessageCreate.MessageData.Attachment?.Media; @@ -250,84 +194,85 @@ long selfUserId entities.Media = new[] { mediaEntity }; // HTMLに整形 - post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink: null); - post.TextFromApi = this.ReplaceTextFromApi(textFromApi, entities, quotedStatusLink: null); - post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi); - post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661"); - post.AccessibleText = CreateAccessibleText(textFromApi, entities, quotedStatus: null, quotedStatusLink: null); - post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText); - post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661"); - post.IsFav = false; - - this.ExtractEntities(entities, post.ReplyToList, post.Media); - - post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null) + var text = CreateHtmlAnchor(rawText, entities, quotedStatusLink: null); + + var textFromApi = this.ReplaceTextFromApi(rawText, entities, quotedStatusLink: null); + textFromApi = WebUtility.HtmlDecode(textFromApi); + textFromApi = textFromApi.Replace("<3", "\u2661"); + + var accessibleText = CreateAccessibleText(rawText, entities, quotedStatus: null, quotedStatusLink: null); + accessibleText = WebUtility.HtmlDecode(accessibleText); + accessibleText = accessibleText.Replace("<3", "\u2661"); + + var (replyToList, media) = this.ExtractEntities(entities); + + var quoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null) .Distinct().ToArray(); - post.ExpandedUrls = entities.OfType() + var expandedUrls = entities.OfType() .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) .ToArray(); // 以下、ユーザー情報 - string userId; - if (eventItem.MessageCreate.SenderId != selfUserId.ToString(CultureInfo.InvariantCulture)) - { - userId = eventItem.MessageCreate.SenderId; - post.IsMe = false; - post.IsOwl = true; - } - else - { - userId = eventItem.MessageCreate.Target.RecipientId; - post.IsMe = true; - post.IsOwl = false; - } + var senderIsMe = eventItem.MessageCreate.SenderId == selfUserId.ToString(CultureInfo.InvariantCulture); + var displayUserId = senderIsMe + ? eventItem.MessageCreate.Target.RecipientId + : eventItem.MessageCreate.SenderId; - if (users.TryGetValue(userId, out var user)) - { - post.UserId = user.Id; - post.ScreenName = user.ScreenName; - post.Nickname = user.Name.Trim(); - post.ImageUrl = user.ProfileImageUrlHttps; - post.IsProtect = user.Protected; - } - else - { - post.UserId = 0L; - post.ScreenName = "?????"; - post.Nickname = "Unknown User"; - } + if (!users.TryGetValue(displayUserId, out var displayUser)) + displayUser = TwitterUser.CreateUnknownUser(); // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる) - if (post.Text == post.TextFromApi) - post.Text = post.TextFromApi; - if (post.AccessibleText == post.TextFromApi) - post.AccessibleText = post.TextFromApi; + if (text == textFromApi) + text = textFromApi; + if (accessibleText == textFromApi) + accessibleText = textFromApi; // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す - post.ScreenName = string.Intern(post.ScreenName); - post.Nickname = string.Intern(post.Nickname); - post.ImageUrl = string.Intern(post.ImageUrl); + var screenName = string.Intern(displayUser.ScreenName); + var nickname = string.Intern(displayUser.Name); + var imageUrl = string.Intern(displayUser.ProfileImageUrlHttps); + var source = (string?)null; + var sourceUri = (Uri?)null; var appId = eventItem.MessageCreate.SourceAppId; if (appId != null && apps.TryGetValue(appId, out var app)) { - post.Source = string.Intern(app.Name); + source = string.Intern(app.Name); try { - post.SourceUri = new Uri(SourceUriBase, app.Url); + sourceUri = new Uri(SourceUriBase, app.Url); } catch (UriFormatException) { } } - post.IsReply = false; - post.IsExcludeReply = false; - post.IsDm = true; - - return post; + return new() + { + StatusId = long.Parse(eventItem.Id), + IsDm = true, + CreatedAt = createdAt, + Text = text, + TextFromApi = textFromApi, + AccessibleText = accessibleText, + QuoteStatusIds = quoteStatusIds, + ExpandedUrls = expandedUrls, + ReplyToList = replyToList, + Media = media, + Source = source ?? "", + SourceUri = sourceUri, + + // displayUser から生成 + UserId = displayUser.Id, + ScreenName = screenName, + Nickname = nickname, + ImageUrl = imageUrl, + IsProtect = displayUser.Protected, + IsMe = senderIsMe, + IsOwl = !senderIsMe, + }; } private string ReplaceTextFromApi(string text, TwitterEntities? entities, TwitterQuotedStatusPermalink? quotedStatusLink) @@ -356,10 +301,13 @@ private string ReplaceTextFromApi(string text, TwitterEntities? entities, Twitte return text; } - private void ExtractEntities(TwitterEntities? entities, List<(long UserId, string ScreenName)> atList, List media) + private (List<(long UserId, string ScreenName)> ReplyToList, List Media) ExtractEntities(TwitterEntities? entities) { + var atList = new List<(long UserId, string ScreenName)>(); + var media = new List(); + if (entities == null) - return; + return (atList, media); if (entities.Hashtags != null) { @@ -377,23 +325,22 @@ private void ExtractEntities(TwitterEntities? entities, List<(long UserId, strin if (entities.Media != null) { - if (media != null) + foreach (var ent in entities.Media) { - foreach (var ent in entities.Media) - { - if (media.Any(x => x.Url == ent.MediaUrlHttps)) - continue; + if (media.Any(x => x.Url == ent.MediaUrlHttps)) + continue; - var videoUrl = - ent.VideoInfo != null && ent.Type == "animated_gif" || ent.Type == "video" - ? ent.ExpandedUrl - : null; + var videoUrl = + ent.VideoInfo != null && ent.Type == "animated_gif" || ent.Type == "video" + ? ent.ExpandedUrl + : null; - var mediaInfo = new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl); - media.Add(mediaInfo); - } + var mediaInfo = new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl); + media.Add(mediaInfo); } } + + return (atList, media); } private static string CreateAccessibleText(string text, TwitterEntities? entities, TwitterStatus? quotedStatus, TwitterQuotedStatusPermalink? quotedStatusLink) From c197a19a9f65a6479cfed745822b1c408b29d0e9 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 15:29:40 +0900 Subject: [PATCH 10/21] =?UTF-8?q?PostClass=E3=82=92=E3=83=AC=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E5=9E=8B=E3=81=AB=E5=A4=89=E6=9B=B4,=20?= =?UTF-8?q?=E5=88=9D=E6=9C=9F=E5=8C=96=E5=BE=8C=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=82=92init-only=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/PostClassTest.cs | 9 - OpenTween.Tests/Models/TabModelTest.cs | 25 +++ OpenTween.Tests/TimelineListViewCacheTest.cs | 58 ++++--- OpenTween/Models/PostClass.cs | 171 ++++++------------- OpenTween/Models/TabModel.cs | 12 ++ OpenTween/Tween.cs | 10 +- OpenTween/Twitter.cs | 29 ++-- 7 files changed, 145 insertions(+), 169 deletions(-) diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 7c0d0fd64..8b1f6902b 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -32,15 +32,6 @@ namespace OpenTween.Models { public class PostClassTest { - [Fact] - public void CloneTest() - { - var post = new PostClass(); - var clonePost = post.Clone(); - - TestUtils.CheckDeepCloning(post, clonePost); - } - [Theory] [InlineData("", "")] [InlineData("aaa\nbbb", "aaa bbb")] diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index 2cf139d1a..a88f7c4e6 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -258,6 +258,31 @@ public void EnqueueRemovePost_SelectedTest() Assert.Equal(new[] { 110L }, tab.SelectedStatusIds); } + [Fact] + public void ReplacePost_SuccessTest() + { + var tab = new PublicSearchTabModel("search"); + var origPost = new PostClass { StatusId = 100L }; + tab.AddPostQueue(origPost); + tab.AddSubmit(); + + Assert.Same(origPost, tab.Posts[100L]); + + var newPost = new PostClass { StatusId = 100L, InReplyToStatusId = 200L }; + Assert.True(tab.ReplacePost(newPost)); + Assert.Same(newPost, tab.Posts[100L]); + } + + [Fact] + public void ReplacePost_FailedTest() + { + var tab = new PublicSearchTabModel("search"); + Assert.False(tab.Contains(100L)); + + var newPost = new PostClass { StatusId = 100L, InReplyToStatusId = 200L }; + Assert.False(tab.ReplacePost(newPost)); + } + [Fact] public void NextUnreadId_Test() { diff --git a/OpenTween.Tests/TimelineListViewCacheTest.cs b/OpenTween.Tests/TimelineListViewCacheTest.cs index cd701866d..0e6f142a2 100644 --- a/OpenTween.Tests/TimelineListViewCacheTest.cs +++ b/OpenTween.Tests/TimelineListViewCacheTest.cs @@ -151,9 +151,11 @@ public void GetItem_RetweetTest() using var listView = new DetailsListView(); using var cache = new TimelineListViewCache(listView, tab, new()); - var post = this.CreatePost(); - post.RetweetedId = 50L; - post.RetweetedBy = "hoge"; + var post = this.CreatePost() with + { + RetweetedId = 50L, + RetweetedBy = "hoge", + }; tab.AddPostQueue(post); tab.AddSubmit(); @@ -313,8 +315,10 @@ public void GetStyle_ForeColor_RetweetTest() using var listView = new DetailsListView(); using var cache = new TimelineListViewCache(listView, tab, new()); - var post = this.CreatePost(); - post.RetweetedId = 100L; + var post = this.CreatePost() with + { + RetweetedId = 100L, + }; tab.AddPostQueue(post); tab.AddSubmit(); @@ -368,9 +372,11 @@ public void GetStyle_ForeColor_DMTest() using var listView = new DetailsListView(); using var cache = new TimelineListViewCache(listView, tab, settingCommon); - var post = this.CreatePost(); - post.IsDm = true; - post.IsOwl = true; + var post = this.CreatePost() with + { + IsDm = true, + IsOwl = true, + }; // DM の場合は設定に関わらず ColorOWL を使う settingCommon.OneWayLove = false; @@ -392,8 +398,10 @@ public void GetStyle_BackColor_AtToTest() var targetPost = this.CreatePost(); tab.AddPostQueue(targetPost); - var basePost = this.CreatePost(); - basePost.InReplyToStatusId = targetPost.StatusId; + var basePost = this.CreatePost() with + { + InReplyToStatusId = targetPost.StatusId, + }; tab.AddPostQueue(basePost); tab.AddSubmit(); @@ -410,8 +418,10 @@ public void GetStyle_BackColor_SelfTest() using var listView = new DetailsListView(); using var cache = new TimelineListViewCache(listView, tab, new()); - var targetPost = this.CreatePost(); - targetPost.IsMe = true; + var targetPost = this.CreatePost() with + { + IsMe = true, + }; tab.AddPostQueue(targetPost); var basePost = this.CreatePost(); @@ -431,8 +441,10 @@ public void GetStyle_BackColor_AtSelfTest() using var listView = new DetailsListView(); using var cache = new TimelineListViewCache(listView, tab, new()); - var targetPost = this.CreatePost(); - targetPost.IsReply = true; + var targetPost = this.CreatePost() with + { + IsReply = true, + }; tab.AddPostQueue(targetPost); var basePost = this.CreatePost(); @@ -455,8 +467,10 @@ public void GetStyle_BackColor_AtFromTargetTest() var targetPost = this.CreatePost(); tab.AddPostQueue(targetPost); - var basePost = this.CreatePost(); - basePost.ReplyToList = new() { (targetPost.UserId, targetPost.ScreenName) }; + var basePost = this.CreatePost() with + { + ReplyToList = new() { (targetPost.UserId, targetPost.ScreenName) }, + }; tab.AddPostQueue(basePost); tab.AddSubmit(); @@ -476,8 +490,10 @@ public void GetStyle_BackColor_AtTargetTest() var basePost = this.CreatePost(); tab.AddPostQueue(basePost); - var targetPost = this.CreatePost(); - targetPost.ReplyToList = new() { (basePost.UserId, basePost.ScreenName) }; + var targetPost = this.CreatePost() with + { + ReplyToList = new() { (basePost.UserId, basePost.ScreenName) }, + }; tab.AddPostQueue(targetPost); tab.AddSubmit(); @@ -497,8 +513,10 @@ public void GetStyle_BackColor_TargetTest() var targetPost = this.CreatePost(); tab.AddPostQueue(targetPost); - var basePost = this.CreatePost(); - basePost.UserId = targetPost.UserId; + var basePost = this.CreatePost() with + { + UserId = targetPost.UserId, + }; tab.AddPostQueue(basePost); tab.AddSubmit(); diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 3651a976e..0c50a6bcd 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -38,27 +38,27 @@ namespace OpenTween.Models { - public class PostClass : ICloneable + public record PostClass() { public readonly record struct StatusGeo( double Longitude, double Latitude ); - public string Nickname { get; set; } = ""; + public string Nickname { get; init; } = ""; - public string TextFromApi { get; set; } = ""; + public string TextFromApi { get; init; } = ""; /// スクリーンリーダーでの読み上げを考慮したテキスト - public string AccessibleText { get; set; } = ""; + public string AccessibleText { get; init; } = ""; - public string ImageUrl { get; set; } = ""; + public string ImageUrl { get; init; } = ""; - public string ScreenName { get; set; } = ""; + public string ScreenName { get; init; } = ""; - public DateTimeUtc CreatedAt { get; set; } + public DateTimeUtc CreatedAt { get; init; } - public long StatusId { get; set; } + public long StatusId { get; init; } public string Text { @@ -73,59 +73,40 @@ public string Text return expandedHtml; } - set => this.text = value; + init => this.text = value; } private string text = ""; - public bool IsRead { get; set; } - - public bool IsReply { get; set; } - - public bool IsExcludeReply { get; set; } - - private bool isProtect; - - public bool IsOwl { get; set; } + public bool IsReply { get; init; } - private bool isMark; + public string? InReplyToUser { get; init; } - public string? InReplyToUser { get; set; } + public string Source { get; init; } = ""; - private long? inReplyToStatusId; + public Uri? SourceUri { get; init; } - public string Source { get; set; } = ""; + public List<(long UserId, string ScreenName)> ReplyToList { get; init; } = new(); - public Uri? SourceUri { get; set; } + public bool IsMe { get; init; } - public List<(long UserId, string ScreenName)> ReplyToList { get; set; } + public bool IsDm { get; init; } - public bool IsMe { get; set; } + public long UserId { get; init; } - public bool IsDm { get; set; } + public string? RetweetedBy { get; init; } - public long UserId { get; set; } - - public bool FilterHit { get; set; } - - public string? RetweetedBy { get; set; } - - public long? RetweetedId { get; set; } - - private bool isDeleted = false; - private StatusGeo? postGeo = null; - - public int RetweetedCount { get; set; } + public long? RetweetedId { get; init; } - public long? RetweetedByUserId { get; set; } + public long? RetweetedByUserId { get; init; } - public long? InReplyToUserId { get; set; } + public long? InReplyToUserId { get; init; } - public List Media { get; set; } + public List Media { get; init; } = new(); - public long[] QuoteStatusIds { get; set; } + public long[] QuoteStatusIds { get; init; } = Array.Empty(); - public ExpandedUrlInfo[] ExpandedUrls { get; set; } + public ExpandedUrlInfo[] ExpandedUrls { get; init; } = Array.Empty(); /// /// に含まれる t.co の展開後の URL を保持するクラス @@ -183,10 +164,6 @@ object ICloneable.Clone() => this.Clone(); } - public int FavoritedCount { get; set; } - - private bool expandComplatedAll = false; - [Flags] private enum States { @@ -197,14 +174,6 @@ private enum States Geo = 8, } - public PostClass() - { - this.Media = new List(); - this.ReplyToList = new List<(long, string)>(); - this.QuoteStatusIds = Array.Empty(); - this.ExpandedUrls = Array.Empty(); - } - public int StateIndex { get @@ -227,17 +196,31 @@ public int StateIndex public string TextSingleLine => this.TextFromApi.Replace("\n", " "); + public long? InReplyToStatusId { get; init; } + + public bool IsProtect { get; init; } + + public StatusGeo? PostGeo { get; set; } + public bool IsFav { get; set; } - public bool IsProtect { get; set; } + public bool IsRead { get; set; } - public bool IsMark { get; set; } + public bool IsExcludeReply { get; set; } + + public bool IsOwl { get; set; } - public long? InReplyToStatusId { get; set; } + public bool IsMark { get; set; } public bool IsDeleted { get; set; } - public StatusGeo? PostGeo { get; set; } + public bool FilterHit { get; set; } + + public int RetweetedCount { get; set; } + + public int FavoritedCount { get; set; } + + private bool expandComplatedAll = false; // 互換性のために用意 public string SourceHtml @@ -299,13 +282,14 @@ public PostClass ConvertToOriginalPost() if (this.RetweetedId == null) throw new InvalidOperationException(); - var originalPost = this.Clone(); - - originalPost.StatusId = this.RetweetedId.Value; - originalPost.RetweetedId = null; - originalPost.RetweetedBy = ""; - originalPost.RetweetedByUserId = null; - originalPost.RetweetedCount = 1; + var originalPost = this with + { + StatusId = this.RetweetedId.Value, + RetweetedId = null, + RetweetedBy = "", + RetweetedByUserId = null, + RetweetedCount = 1, + }; return originalPost; } @@ -350,60 +334,5 @@ private string ReplaceToExpandedUrl(string html, out bool completedAll) return html; } - - public PostClass Clone() - { - var clone = (PostClass)this.MemberwiseClone(); - clone.ReplyToList = new List<(long, string)>(this.ReplyToList); - clone.Media = new List(this.Media); - clone.QuoteStatusIds = this.QuoteStatusIds.ToArray(); - clone.ExpandedUrls = this.ExpandedUrls.Select(x => x.Clone()).ToArray(); - - return clone; - } - - object ICloneable.Clone() - => this.Clone(); - - public override bool Equals(object? obj) - { - if (obj == null || this.GetType() != obj.GetType()) return false; - return this.Equals((PostClass)obj); - } - - public bool Equals(PostClass? other) - { - if (other == null) return false; - return (this.Nickname == other.Nickname) && - (this.TextFromApi == other.TextFromApi) && - (this.ImageUrl == other.ImageUrl) && - (this.ScreenName == other.ScreenName) && - (this.CreatedAt == other.CreatedAt) && - (this.StatusId == other.StatusId) && - (this.IsFav == other.IsFav) && - (this.Text == other.Text) && - (this.IsRead == other.IsRead) && - (this.IsReply == other.IsReply) && - (this.IsExcludeReply == other.IsExcludeReply) && - (this.IsProtect == other.IsProtect) && - (this.IsOwl == other.IsOwl) && - (this.IsMark == other.IsMark) && - (this.InReplyToUser == other.InReplyToUser) && - (this.InReplyToStatusId == other.InReplyToStatusId) && - (this.Source == other.Source) && - (this.SourceUri == other.SourceUri) && - this.ReplyToList.SequenceEqual(other.ReplyToList) && - (this.IsMe == other.IsMe) && - (this.IsDm == other.IsDm) && - (this.UserId == other.UserId) && - (this.FilterHit == other.FilterHit) && - (this.RetweetedBy == other.RetweetedBy) && - (this.RetweetedId == other.RetweetedId) && - (this.IsDeleted == other.IsDeleted) && - (this.InReplyToUserId == other.InReplyToUserId); - } - - public override int GetHashCode() - => this.StatusId.GetHashCode(); } } diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index d9c054279..7eb055ba6 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -194,6 +194,18 @@ public IReadOnlyList RemoveSubmit() return removedIds; } + /// + /// タブ内にある が一致する発言を置き換える + /// + /// 置き換えに成功した場合は true、タブ内に存在しない発言などで失敗した場合は false + public bool ReplacePost(PostClass post) + { + if (!this.Posts.TryGetValue(post.StatusId, out var origPost)) + return false; + + return this.Posts.TryUpdate(post.StatusId, post, origPost); + } + public void SelectPosts(int[] indices) { bool IsValidIndex(int index) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 1e5bb4756..cb6124bb7 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -5316,9 +5316,13 @@ private async Task GoInReplyToPostTree() { var post = await this.tw.GetStatusApi(false, currentPost.StatusId); - currentPost.InReplyToStatusId = post.InReplyToStatusId; - currentPost.InReplyToUser = post.InReplyToUser; - currentPost.IsReply = post.IsReply; + currentPost = currentPost with + { + InReplyToStatusId = post.InReplyToStatusId, + InReplyToUser = post.InReplyToUser, + IsReply = post.IsReply, + }; + curTabClass.ReplacePost(currentPost); this.listCache?.PurgeCache(); var index = curTabClass.SelectedIndex; diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index 7aa503dbb..78ebf84f7 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -388,17 +388,12 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) throw new WebApiException("Invalid Json!"); // Retweetしたものを返す - post = this.CreatePostsFromStatusData(status); - - // ユーザー情報 - post.IsMe = true; - - post.IsRead = read; - post.IsOwl = false; - if (this.ReadOwnPost) post.IsRead = true; - post.IsDm = false; - - return post; + return this.CreatePostsFromStatusData(status) with + { + IsMe = true, + IsRead = this.ReadOwnPost ? true : read, + IsOwl = false, + }; } public string Username @@ -790,10 +785,12 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) if (targetPost.RetweetedId != null) { - var originalPost = targetPost.Clone(); - originalPost.StatusId = targetPost.RetweetedId.Value; - originalPost.RetweetedId = null; - originalPost.RetweetedBy = null; + var originalPost = targetPost with + { + StatusId = targetPost.RetweetedId.Value, + RetweetedId = null, + RetweetedBy = null, + }; targetPost = originalPost; } @@ -900,7 +897,7 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) relPosts.Values.ToList().ForEach(p => { - var post = p.Clone(); + var post = p with { }; if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true; else From e005881de2bd98ed225800443c1351e97b5d13a7 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 25 Jun 2023 19:44:14 +0900 Subject: [PATCH 11/21] =?UTF-8?q?PostClass.StatusId=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=83=9F=E3=83=AA=E7=A7=92=E7=B2=BE=E5=BA=A6=E3=81=AE=E6=97=A5?= =?UTF-8?q?=E6=99=82=E3=82=92=E6=8A=BD=E5=87=BA=E3=81=97=E3=81=A6=E3=82=BD?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AB=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将来的に Mastodon 等に対応した場合に Twitter のツイートと混在した 発言一覧を表示するためには StatusId に依存せず日時順にソートする機能を備える必要がある --- OpenTween.Tests/DateTimeUtcTest.cs | 9 + OpenTween.Tests/Models/PostClassTest.cs | 4 + OpenTween.Tests/Models/TabModelTest.cs | 228 +++++++++++++++--- .../Models/TwitterPostFactoryTest.cs | 28 +++ OpenTween/DateTimeUtc.cs | 3 + OpenTween/Models/PostClass.cs | 8 + OpenTween/Models/TabModel.cs | 46 ++-- OpenTween/Models/TwitterPostFactory.cs | 23 +- 8 files changed, 292 insertions(+), 57 deletions(-) diff --git a/OpenTween.Tests/DateTimeUtcTest.cs b/OpenTween.Tests/DateTimeUtcTest.cs index 8f9d9f167..1126c3269 100644 --- a/OpenTween.Tests/DateTimeUtcTest.cs +++ b/OpenTween.Tests/DateTimeUtcTest.cs @@ -269,6 +269,15 @@ public void FromUnixTime_Test() utc.ToDateTimeUnsafe()); } + [Fact] + public void FromUnixTimeMilliseconds_Test() + { + var utc = DateTimeUtc.FromUnixTimeMilliseconds(1234567890123); + + Assert.Equal(new DateTime(2009, 2, 13, 23, 31, 30, 123, DateTimeKind.Utc), + utc.ToDateTimeUnsafe()); + } + public static readonly TheoryData ParseTestFixtures = new() { { "2018-05-06T11:22:33.111", new DateTimeUtc(2018, 5, 6, 11, 22, 33, 111) }, diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 8b1f6902b..4809575a1 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -272,6 +272,8 @@ public void ConvertToOriginalPost_Test() var retweetPost = new PostClass { StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 2, 0, 0, 0), + CreatedAt = new(2023, 1, 1, 0, 0, 0), ScreenName = "@aaa", UserId = 1L, @@ -284,6 +286,8 @@ public void ConvertToOriginalPost_Test() var originalPost = retweetPost.ConvertToOriginalPost(); Assert.Equal(50L, originalPost.StatusId); + Assert.Equal(new(2023, 1, 1, 0, 0, 0), originalPost.CreatedAt); + Assert.Equal(new(2023, 1, 1, 0, 0, 0), originalPost.CreatedAtForSorting); Assert.Equal("@aaa", originalPost.ScreenName); Assert.Equal(1L, originalPost.UserId); diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index a88f7c4e6..f66696ddd 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -337,16 +337,31 @@ public void NextUnreadId_SortByIdAscTest() tab.UnreadManage = true; - // ID の昇順でソート + // ID (CreatedAtForSorting) の昇順でソート tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); // 画面には上から 100 → 200 → 300 の順に並ぶ - tab.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 300L, IsRead = false }); + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), + IsRead = false, + }); + tab.AddPostQueue(new() + { + StatusId = 200L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), + IsRead = false, + }); + tab.AddPostQueue(new() + { + StatusId = 300L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), + IsRead = false, + }); tab.AddSubmit(); - // 昇順/降順に関わらず、ID の小さい順に未読の ID を返す + // 昇順/降順に関わらず、CreatedAtForSorting の小さい順に未読の ID を返す Assert.Equal(100L, tab.NextUnreadId); } @@ -357,16 +372,31 @@ public void NextUnreadId_SortByIdDescTest() tab.UnreadManage = true; - // ID の降順でソート + // ID (CreatedAtForSorting) の降順でソート tab.SetSortMode(ComparerMode.Id, SortOrder.Descending); // 画面には上から 300 → 200 → 100 の順に並ぶ - tab.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 300L, IsRead = false }); + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), + IsRead = false, + }); + tab.AddPostQueue(new() + { + StatusId = 200L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), + IsRead = false, + }); + tab.AddPostQueue(new() + { + StatusId = 300L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), + IsRead = false, + }); tab.AddSubmit(); - // 昇順/降順に関わらず、ID の小さい順に未読の ID を返す + // 昇順/降順に関わらず、CreatedAtForSorting の小さい順に未読の ID を返す Assert.Equal(100L, tab.NextUnreadId); } @@ -471,16 +501,19 @@ public void NextUnreadIndex_Test() tab.AddPostQueue(new PostClass { StatusId = 50L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = true, // 既読 }); tab.AddPostQueue(new PostClass { StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), IsRead = false, // 未読 }); tab.AddPostQueue(new PostClass { StatusId = 150L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), IsRead = false, // 未読 }); tab.AddSubmit(); @@ -500,6 +533,7 @@ public void NextUnreadIndex_DisabledTest() tab.AddPostQueue(new PostClass { StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = false, // 未読 }); tab.AddSubmit(); @@ -644,11 +678,46 @@ public void SearchPostsAll_Test() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd", ScreenName = "", Nickname = "" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh", ScreenName = "", Nickname = "" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl", ScreenName = "", Nickname = "" }); // 2 - tab.AddPostQueue(new PostClass { StatusId = 130L, TextFromApi = "abc", ScreenName = "", Nickname = "" }); // 3 - tab.AddPostQueue(new PostClass { StatusId = 140L, TextFromApi = "def", ScreenName = "", Nickname = "" }); // 4 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 130L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), // 3 + TextFromApi = "abc", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 140L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 4), // 4 + TextFromApi = "def", + ScreenName = "", + Nickname = "", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); @@ -667,11 +736,46 @@ public void SearchPostsAll_ReverseOrderTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd", ScreenName = "", Nickname = "" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh", ScreenName = "", Nickname = "" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl", ScreenName = "", Nickname = "" }); // 2 - tab.AddPostQueue(new PostClass { StatusId = 130L, TextFromApi = "abc", ScreenName = "", Nickname = "" }); // 3 - tab.AddPostQueue(new PostClass { StatusId = 140L, TextFromApi = "def", ScreenName = "", Nickname = "" }); // 4 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 130L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), // 3 + TextFromApi = "abc", + ScreenName = "", + Nickname = "", + }); + tab.AddPostQueue(new() + { + StatusId = 140L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 4), // 4 + TextFromApi = "def", + ScreenName = "", + Nickname = "", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); @@ -690,9 +794,24 @@ public void GetterSingle_Test() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl" }); // 2 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); @@ -706,9 +825,24 @@ public void GetterSingle_ErrorTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl" }); // 2 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); @@ -722,9 +856,24 @@ public void GetterSlice_Test() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl" }); // 2 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); @@ -739,9 +888,24 @@ public void GetterSlice_ErrorTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L, TextFromApi = "abcd" }); // 0 - tab.AddPostQueue(new PostClass { StatusId = 110L, TextFromApi = "efgh" }); // 1 - tab.AddPostQueue(new PostClass { StatusId = 120L, TextFromApi = "ijkl" }); // 2 + tab.AddPostQueue(new() + { + StatusId = 100L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 + TextFromApi = "abcd", + }); + tab.AddPostQueue(new() + { + StatusId = 110L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 + TextFromApi = "efgh", + }); + tab.AddPostQueue(new() + { + StatusId = 120L, + CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 + TextFromApi = "ijkl", + }); tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index a62d8a347..c2f7a9861 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -639,5 +639,33 @@ public void GetQuoteTweetStatusIds_OverflowTest() var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls); Assert.Empty(statusIds); } + + [Fact] + public void ParseDateTimeFromSnowflakeId_LowerTest() + { + var statusId = 1659990873340346368L; + var createdAtStr = "Sat May 20 18:34:00 +0000 2023"; + var expected = new DateTimeUtc(2023, 5, 20, 18, 34, 0, 0); + Assert.Equal(expected, TwitterPostFactory.ParseDateTimeFromSnowflakeId(statusId, createdAtStr)); + } + + [Fact] + public void ParseDateTimeFromSnowflakeId_UpperTest() + { + var statusId = 1672312060766748673L; + var createdAtStr = "Fri Jun 23 18:33:59 +0000 2023"; + var expected = new DateTimeUtc(2023, 6, 23, 18, 33, 59, 999); + Assert.Equal(expected, TwitterPostFactory.ParseDateTimeFromSnowflakeId(statusId, createdAtStr)); + } + + [Fact] + public void ParseDateTimeFromSnowflakeId_FallbackTest() + { + // Snowflake 導入以前の status_id に対しては created_at の文字列からパースした日時を採用する + var statusId = 20L; + var createdAtStr = "Tue Mar 21 20:50:14 +0000 2006"; + var expected = new DateTimeUtc(2006, 3, 21, 20, 50, 14, 0); + Assert.Equal(expected, TwitterPostFactory.ParseDateTimeFromSnowflakeId(statusId, createdAtStr)); + } } } diff --git a/OpenTween/DateTimeUtc.cs b/OpenTween/DateTimeUtc.cs index b89abd391..cc05e2b44 100644 --- a/OpenTween/DateTimeUtc.cs +++ b/OpenTween/DateTimeUtc.cs @@ -152,6 +152,9 @@ public string ToLocalTimeString(string format) public static DateTimeUtc FromUnixTime(long unixTime) => UnixEpoch + TimeSpan.FromTicks(unixTime * TimeSpan.TicksPerSecond); + public static DateTimeUtc FromUnixTimeMilliseconds(long unixTimeMs) + => UnixEpoch + TimeSpan.FromTicks(unixTimeMs * TimeSpan.TicksPerMillisecond); + public static DateTimeUtc Parse(string input, IFormatProvider formatProvider) => new(DateTimeOffset.Parse(input, formatProvider, DateTimeStyles.AssumeUniversal)); diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 0c50a6bcd..ca5bc312a 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -58,6 +58,13 @@ double Latitude public DateTimeUtc CreatedAt { get; init; } + /// ソート用の日時 + /// + /// はリツイートの場合にRT元の日時を表すため、 + /// ソート用に使用するタイムスタンプを保持する必要がある + /// + public DateTimeUtc CreatedAtForSorting { get; init; } + public long StatusId { get; init; } public string Text @@ -285,6 +292,7 @@ public PostClass ConvertToOriginalPost() var originalPost = this with { StatusId = this.RetweetedId.Value, + CreatedAtForSorting = this.CreatedAt, RetweetedId = null, RetweetedBy = "", RetweetedByUserId = null, diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index 7eb055ba6..4279ed363 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -255,34 +255,32 @@ private void ApplySortMode() { var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1; - Comparison comparison; - if (this.SortMode == ComparerMode.Id) + Comparison postComparison = this.SortMode switch { - comparison = (x, y) => sign * x.CompareTo(y); - } - else + ComparerMode.Id => + (x, y) => Comparer.Default.Compare(x?.CreatedAtForSorting, y?.CreatedAtForSorting), + ComparerMode.Name => + (x, y) => Comparer.Default.Compare(x?.ScreenName, y?.ScreenName), + ComparerMode.Nickname => + (x, y) => Comparer.Default.Compare(x?.Nickname, y?.Nickname), + ComparerMode.Source => + (x, y) => Comparer.Default.Compare(x?.Source, y?.Source), + _ => + (x, y) => Comparer.Default.Compare(x?.TextFromApi, y?.TextFromApi), + }; + + Comparison comparison = (x, y) => { - Comparison postComparison = this.SortMode switch - { - ComparerMode.Name => (x, y) => Comparer.Default.Compare(x?.ScreenName, y?.ScreenName), - ComparerMode.Nickname => (x, y) => Comparer.Default.Compare(x?.Nickname, y?.Nickname), - ComparerMode.Source => (x, y) => Comparer.Default.Compare(x?.Source, y?.Source), - _ => (x, y) => Comparer.Default.Compare(x?.TextFromApi, y?.TextFromApi), - }; + this.Posts.TryGetValue(x, out var xPost); + this.Posts.TryGetValue(y, out var yPost); - comparison = (x, y) => - { - this.Posts.TryGetValue(x, out var xPost); - this.Posts.TryGetValue(y, out var yPost); - - var compare = sign * postComparison(xPost, yPost); - if (compare != 0) - return compare; + var compare = sign * postComparison(xPost, yPost); + if (compare != 0) + return compare; - // 同値であれば status_id で比較する - return sign * x.CompareTo(y); - }; - } + // 同値であれば status_id で比較する + return sign * x.CompareTo(y); + }; var comparer = Comparer.Create(comparison); diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index c29cbbc82..b523de6cf 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -133,14 +133,20 @@ public PostClass CreateFromStatus( if (followerIds.Count > 0) isOwl = !followerIds.Contains(originalStatusUser.Id); + var createdAtForSorting = ParseDateTimeFromSnowflakeId(status.Id, status.CreatedAt); + var createdAt = retweetedStatus != null + ? ParseDateTimeFromSnowflakeId(retweetedStatus.Id, retweetedStatus.CreatedAt) + : createdAtForSorting; + return new() { // status から生成 StatusId = status.Id, + CreatedAtForSorting = createdAtForSorting, IsMe = statusUser.Id == selfUserId, // originalStatus から生成 - CreatedAt = MyCommon.DateTimeParse(originalStatus.CreatedAt), + CreatedAt = createdAt, Text = text, TextFromApi = textFromApi, AccessibleText = accessibleText, @@ -513,5 +519,20 @@ public static IEnumerable GetQuoteTweetStatusIds(IEnumerable urls) } } } + + public static readonly DateTimeUtc TwitterEpoch = DateTimeUtc.FromUnixTimeMilliseconds(1288834974657L); + + public static DateTimeUtc ParseDateTimeFromSnowflakeId(long statusId, string createdAtStr) + { + // status_id からミリ秒単位の日時を算出する + var timestampInMs = TwitterEpoch + TimeSpan.FromMilliseconds(statusId >> 22); + + // 通常の方法で得た秒精度の日時と比較して 1 秒未満の差であれば timestampInMs の値を採用する + // (Snowflake 導入以前の ID や仕様変更によりこの計算式が使えなくなった場合の対策) + var createdAtFromStr = MyCommon.DateTimeParse(createdAtStr); + var correct = (timestampInMs - createdAtFromStr).Duration() < TimeSpan.FromSeconds(1); + + return correct ? timestampInMs : createdAtFromStr; + } } } From 7bdb567f49a2b14e9dabd30217d831da91119882 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 2 Jul 2023 01:44:51 +0900 Subject: [PATCH 12/21] =?UTF-8?q?TabModel.SelectedStatusId=E3=82=92Nullabl?= =?UTF-8?q?e=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/TabModelTest.cs | 2 +- OpenTween/Models/TabModel.cs | 6 +++--- OpenTween/Tween.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index f66696ddd..26fceab07 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -129,7 +129,7 @@ public void SelectPosts_EmptyTest() tab.SelectPosts(Array.Empty()); Assert.Empty(tab.SelectedStatusIds); - Assert.Equal(-1L, tab.SelectedStatusId); + Assert.Null(tab.SelectedStatusId); Assert.Empty(tab.SelectedPosts); Assert.Null(tab.SelectedPost); Assert.Equal(-1, tab.SelectedIndex); diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index 4279ed363..c467f5291 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -82,8 +82,8 @@ public virtual ConcurrentDictionary Posts public long[] SelectedStatusIds => this.selectedStatusIds.ToArray(); - public long SelectedStatusId - => this.selectedStatusIds.DefaultIfEmpty(-1).First(); + public long? SelectedStatusId + => this.selectedStatusIds.Count > 0 ? this.selectedStatusIds[0] : null; public PostClass[] SelectedPosts => this.selectedStatusIds.Select(x => this.Posts[x]).ToArray(); @@ -96,7 +96,7 @@ public int SelectedIndex get { var statusId = this.SelectedStatusId; - return statusId != -1 ? this.IndexOf(statusId) : -1; + return statusId != null ? this.IndexOf(statusId.Value) : -1; } } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index cb6124bb7..9643baed4 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -5019,7 +5019,7 @@ private void GoSamePostToAnotherTab(bool left) return; var selectedStatusId = tab.SelectedStatusId; - if (selectedStatusId == -1) + if (selectedStatusId == null) return; int fIdx, toIdx, stp; @@ -5061,7 +5061,7 @@ private void GoSamePostToAnotherTab(bool left) if (targetTab.TabType == MyCommon.TabUsageType.DirectMessage) continue; - var foundIndex = targetTab.IndexOf(selectedStatusId); + var foundIndex = targetTab.IndexOf(selectedStatusId.Value); if (foundIndex != -1) { this.ListTab.SelectedIndex = tabidx; From 74e2b8098d609ca84e3c24207a53f9a3a7db5bca Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 2 Jul 2023 01:51:13 +0900 Subject: [PATCH 13/21] =?UTF-8?q?TabModel.NextUnreadId=E3=82=92Nullable?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Models/TabModel.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index c467f5291..5dd49f52f 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -290,17 +290,17 @@ private void ApplySortMode() /// /// 次に表示する未読ツイートのIDを返します。 - /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します + /// ただし、未読がない場合または UnreadManage が false の場合は null を返します /// - public long NextUnreadId + public long? NextUnreadId { get { if (!this.UnreadManage || !SettingManager.Instance.Common.UnreadManage) - return -1L; + return null; if (this.unreadIds.Count == 0) - return -1L; + return null; // unreadIds はリストのインデックス番号順に並んでいるため、 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる @@ -317,7 +317,7 @@ public int NextUnreadIndex get { var unreadId = this.NextUnreadId; - return unreadId != -1 ? this.IndexOf(unreadId) : -1; + return unreadId != null ? this.IndexOf(unreadId.Value) : -1; } } From 76c56a01908f4bcdbe1a6411a7e9e91effcd8b4e Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 2 Jul 2023 01:26:54 +0900 Subject: [PATCH 14/21] =?UTF-8?q?=E7=99=BA=E8=A8=80ID=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E7=8F=BE=E3=81=99=E3=82=8BPostId=E3=82=AF=E3=83=A9=E3=82=B9?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 現状では TwitterStatusId 以外の型が使われてもエラーになる --- OpenTween.Tests/Api/TwitterApiTest.cs | 14 +- OpenTween.Tests/Models/PostClassTest.cs | 10 +- OpenTween.Tests/Models/PostFilterRuleTest.cs | 4 +- OpenTween.Tests/Models/PostIdTest.cs | 119 +++++++++ OpenTween.Tests/Models/TabInformationTest.cs | 238 +++++++++--------- OpenTween.Tests/Models/TabModelTest.cs | 210 ++++++++-------- .../Models/TwitterPostFactoryTest.cs | 8 +- OpenTween.Tests/MyCommonTest.cs | 6 +- OpenTween.Tests/TimelineListViewCacheTest.cs | 6 +- OpenTween.Tests/TweetDetailsViewTest.cs | 6 +- OpenTween.Tests/TwitterTest.cs | 30 +-- OpenTween/Api/TwitterApi.cs | 29 +-- OpenTween/Models/InternalStorageTabModel.cs | 10 +- OpenTween/Models/PostClass.cs | 10 +- OpenTween/Models/PostId.cs | 68 +++++ OpenTween/Models/TabInformations.cs | 23 +- OpenTween/Models/TabModel.cs | 64 ++--- OpenTween/Models/TwitterDirectMessageId.cs | 41 +++ OpenTween/Models/TwitterPostFactory.cs | 18 +- OpenTween/Models/TwitterStatusId.cs | 50 ++++ OpenTween/MyCommon.cs | 10 +- OpenTween/PostStatusParams.cs | 3 +- OpenTween/TimelineListViewState.cs | 26 +- OpenTween/Tween.cs | 97 ++++--- OpenTween/TweetDetailsView.cs | 12 +- OpenTween/Twitter.cs | 52 ++-- 26 files changed, 723 insertions(+), 441 deletions(-) create mode 100644 OpenTween.Tests/Models/PostIdTest.cs create mode 100644 OpenTween/Models/PostId.cs create mode 100644 OpenTween/Models/TwitterDirectMessageId.cs create mode 100644 OpenTween/Models/TwitterStatusId.cs diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 85ea02e00..cd48cc5ad 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -191,7 +191,7 @@ public async Task StatusesShow_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesShow(statusId: 100L) + await twitterApi.StatusesShow(statusId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -252,7 +252,7 @@ public async Task StatusesUpdate_Test() await twitterApi.StatusesUpdate( "hogehoge", - replyToId: 100L, + replyToId: new("100"), mediaIds: new[] { 10L, 20L }, autoPopulateReplyMetadata: true, excludeReplyUserIds: new[] { 100L, 200L }, @@ -306,7 +306,7 @@ public async Task StatusesDestroy_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesDestroy(statusId: 100L) + await twitterApi.StatusesDestroy(statusId: new("100")) .IgnoreResponse() .ConfigureAwait(false); @@ -333,7 +333,7 @@ public async Task StatusesRetweet_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesRetweet(100L) + await twitterApi.StatusesRetweet(new("100")) .IgnoreResponse() .ConfigureAwait(false); @@ -748,7 +748,7 @@ public async Task DirectMessagesEventsDestroy_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.DirectMessagesEventsDestroy(eventId: "100") + await twitterApi.DirectMessagesEventsDestroy(eventId: new("100")) .ConfigureAwait(false); mock.VerifyAll(); @@ -880,7 +880,7 @@ public async Task FavoritesCreate_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.FavoritesCreate(statusId: 100L) + await twitterApi.FavoritesCreate(statusId: new("100")) .IgnoreResponse() .ConfigureAwait(false); @@ -905,7 +905,7 @@ public async Task FavoritesDestroy_Test() using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); twitterApi.ApiConnection = mock.Object; - await twitterApi.FavoritesDestroy(statusId: 100L) + await twitterApi.FavoritesDestroy(statusId: new("100")) .IgnoreResponse() .ConfigureAwait(false); diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 4809575a1..943e742e7 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -67,7 +67,7 @@ public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int ex { IsProtect = protect, IsMark = mark, - InReplyToStatusId = reply ? (long?)100L : null, + InReplyToStatusId = reply ? new TwitterStatusId("100") : null, PostGeo = geo ? new PostClass.StatusGeo(-126.716667, -47.15) : (PostClass.StatusGeo?)null, }; @@ -271,13 +271,13 @@ public void ConvertToOriginalPost_Test() { var retweetPost = new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 2, 0, 0, 0), CreatedAt = new(2023, 1, 1, 0, 0, 0), ScreenName = "@aaa", UserId = 1L, - RetweetedId = 50L, + RetweetedId = new TwitterStatusId("50"), RetweetedBy = "@bbb", RetweetedByUserId = 2L, RetweetedCount = 0, @@ -285,7 +285,7 @@ public void ConvertToOriginalPost_Test() var originalPost = retweetPost.ConvertToOriginalPost(); - Assert.Equal(50L, originalPost.StatusId); + Assert.Equal(new TwitterStatusId("50"), originalPost.StatusId); Assert.Equal(new(2023, 1, 1, 0, 0, 0), originalPost.CreatedAt); Assert.Equal(new(2023, 1, 1, 0, 0, 0), originalPost.CreatedAtForSorting); Assert.Equal("@aaa", originalPost.ScreenName); @@ -301,7 +301,7 @@ public void ConvertToOriginalPost_Test() public void ConvertToOriginalPost_ErrorTest() { // 公式 RT でないツイート - var post = new PostClass { StatusId = 100L, RetweetedId = null }; + var post = new PostClass { StatusId = new TwitterStatusId("100"), RetweetedId = null }; Assert.Throws(() => post.ConvertToOriginalPost()); } diff --git a/OpenTween.Tests/Models/PostFilterRuleTest.cs b/OpenTween.Tests/Models/PostFilterRuleTest.cs index 5c6846ad7..a83b42210 100644 --- a/OpenTween.Tests/Models/PostFilterRuleTest.cs +++ b/OpenTween.Tests/Models/PostFilterRuleTest.cs @@ -1410,7 +1410,7 @@ public void FilterRt_Test() filter.FilterRt = true; - post = new PostClass { RetweetedBy = "hogehoge", RetweetedId = 123L }; + post = new PostClass { RetweetedBy = "hogehoge", RetweetedId = new TwitterStatusId("123") }; Assert.Equal(MyCommon.HITRESULT.CopyAndMark, filter.ExecFilter(post)); post = new PostClass { }; @@ -1425,7 +1425,7 @@ public void ExFilterRt_Test() filter.ExFilterRt = true; - post = new PostClass { RetweetedBy = "hogehoge", RetweetedId = 123L }; + post = new PostClass { RetweetedBy = "hogehoge", RetweetedId = new TwitterStatusId("123") }; Assert.Equal(MyCommon.HITRESULT.Exclude, filter.ExecFilter(post)); post = new PostClass { }; diff --git a/OpenTween.Tests/Models/PostIdTest.cs b/OpenTween.Tests/Models/PostIdTest.cs new file mode 100644 index 000000000..7ac665c87 --- /dev/null +++ b/OpenTween.Tests/Models/PostIdTest.cs @@ -0,0 +1,119 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace OpenTween.Models +{ + public class PostIdTest + { + private PostId CreatePostId(string type, string id) + { + var mock = new Mock() { CallBase = true }; + mock.Setup(x => x.IdType).Returns(type); + mock.Setup(x => x.Id).Returns(id); + return mock.Object; + } + + [Fact] + public void CompareTo_Test() + { + var a = this.CreatePostId("mastodon", "200"); + var b = this.CreatePostId("twitter", "100"); + Assert.True(a.CompareTo(b) < 0); + Assert.True(b.CompareTo(a) > 0); + Assert.Equal(0, a.CompareTo(a)); + } + + [Fact] + public void CompareTo_SameIdTypeTest() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "200"); + Assert.True(a.CompareTo(b) < 0); + Assert.True(b.CompareTo(a) > 0); + Assert.Equal(0, a.CompareTo(a)); + } + + [Fact] + public void Equals_Test() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "100"); + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + Assert.True(a == b); + Assert.True(b == a); + } + + [Fact] + public void Equals_NotSameIdTypeTest() + { + var a = this.CreatePostId("mastodon", "100"); + var b = this.CreatePostId("twitter", "100"); + Assert.False(a.Equals(b)); + Assert.False(b.Equals(a)); + Assert.True(a != b); + Assert.True(b != a); + } + + [Fact] + public void Equals_NotSameIdTest() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "200"); + Assert.False(a.Equals(b)); + Assert.False(b.Equals(a)); + Assert.True(a != b); + Assert.True(b != a); + } + + [Fact] + public void GetHashCode_SameIdTest() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "100"); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_NotSameIdTypeTest() + { + var a = this.CreatePostId("mastodon", "100"); + var b = this.CreatePostId("twitter", "100"); + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_NotSameIdTest() + { + var a = this.CreatePostId("twitter", "100"); + var b = this.CreatePostId("twitter", "200"); + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + } +} diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index bc80d4e3d..cfe2279c0 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -75,7 +75,7 @@ public void AddTab_DuplicateTest() public void RemoveTab_InnerStorageTabTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); this.tabinfo.AddTab(tab); this.tabinfo.SubmitUpdate(); @@ -96,12 +96,12 @@ public void RemoveTab_FilterTab_MovedPost_OrphanedTest() filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); this.tabinfo.AddTab(filterTab); - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "opentween" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); - Assert.False(this.tabinfo.HomeTab.Contains(100L)); - Assert.True(filterTab.Contains(100L)); + Assert.False(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab.Contains(new TwitterStatusId("100"))); this.tabinfo.RemoveTab("filter"); @@ -110,7 +110,7 @@ public void RemoveTab_FilterTab_MovedPost_OrphanedTest() Assert.Contains(filterTab, this.tabinfo.RemovedTab); // 他に MoveMatches で移動している振り分けタブが存在しなければ Home タブに戻す - Assert.True(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); } [Fact] @@ -124,13 +124,13 @@ public void RemoveTab_FilterTab_MovedPost_NotOrphanedTest() filterTab2.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); this.tabinfo.AddTab(filterTab2); - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "opentween" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); - Assert.False(this.tabinfo.HomeTab.Contains(100L)); - Assert.True(filterTab1.Contains(100L)); - Assert.True(filterTab2.Contains(100L)); + Assert.False(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab1.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab2.Contains(new TwitterStatusId("100"))); this.tabinfo.RemoveTab("filter1"); @@ -139,8 +139,8 @@ public void RemoveTab_FilterTab_MovedPost_NotOrphanedTest() Assert.Contains(filterTab1, this.tabinfo.RemovedTab); // 他に MoveMatches で移動している振り分けタブが存在する場合は Home タブに戻さない - Assert.False(this.tabinfo.HomeTab.Contains(100L)); - Assert.True(filterTab2.Contains(100L)); + Assert.False(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab2.Contains(new TwitterStatusId("100"))); } [Fact] @@ -150,12 +150,12 @@ public void RemoveTab_FilterTab_CopiedPost_Test() filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = false }); this.tabinfo.AddTab(filterTab); - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "opentween" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); - Assert.True(this.tabinfo.HomeTab.Contains(100L)); - Assert.True(filterTab.Contains(100L)); + Assert.True(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab.Contains(new TwitterStatusId("100"))); this.tabinfo.RemoveTab("filter"); @@ -164,7 +164,7 @@ public void RemoveTab_FilterTab_CopiedPost_Test() Assert.Contains(filterTab, this.tabinfo.RemovedTab); // 振り分けタブにコピーされた発言は Home タブにも存在しているため何もしない - Assert.True(this.tabinfo.HomeTab.Contains(100L)); + Assert.True(this.tabinfo.HomeTab.Contains(new TwitterStatusId("100"))); } [Fact] @@ -612,15 +612,15 @@ public void SetReadAllTab_MarkAsReadTest() // search1 に追加するツイート (StatusId: 100, 150, 200; すべて未読) tab1.UnreadManage = true; - tab1.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); - tab1.AddPostQueue(new PostClass { StatusId = 150L, IsRead = false }); - tab1.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = false }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = false }); // search2 に追加するツイート (StatusId: 150, 200, 250; すべて未読) tab2.UnreadManage = true; - tab2.AddPostQueue(new PostClass { StatusId = 150L, IsRead = false }); - tab2.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false }); - tab2.AddPostQueue(new PostClass { StatusId = 250L, IsRead = false }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = false }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = false }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("250"), IsRead = false }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); @@ -632,12 +632,12 @@ public void SetReadAllTab_MarkAsReadTest() // ... ここまで長い前置き // StatusId: 200 を既読にする (search1, search2 両方に含まれる) - this.tabinfo.SetReadAllTab(200L, read: true); + this.tabinfo.SetReadAllTab(new TwitterStatusId("200"), read: true); Assert.Equal(2, tab1.UnreadCount); Assert.Equal(2, tab2.UnreadCount); // StatusId: 100 を既読にする (search1 のみに含まれる) - this.tabinfo.SetReadAllTab(100L, read: true); + this.tabinfo.SetReadAllTab(new TwitterStatusId("100"), read: true); Assert.Equal(1, tab1.UnreadCount); Assert.Equal(2, tab2.UnreadCount); } @@ -653,15 +653,15 @@ public void SetReadAllTab_MarkAsUnreadTest() // search1 に追加するツイート (StatusId: 100, 150, 200; すべて既読) tab1.UnreadManage = true; - tab1.AddPostQueue(new PostClass { StatusId = 100L, IsRead = true }); - tab1.AddPostQueue(new PostClass { StatusId = 150L, IsRead = true }); - tab1.AddPostQueue(new PostClass { StatusId = 200L, IsRead = true }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = true }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = true }); + tab1.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = true }); // search2 に追加するツイート (StatusId: 150, 200, 250; すべて既読) tab2.UnreadManage = true; - tab2.AddPostQueue(new PostClass { StatusId = 150L, IsRead = true }); - tab2.AddPostQueue(new PostClass { StatusId = 200L, IsRead = true }); - tab2.AddPostQueue(new PostClass { StatusId = 250L, IsRead = true }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = true }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = true }); + tab2.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("250"), IsRead = true }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); @@ -673,12 +673,12 @@ public void SetReadAllTab_MarkAsUnreadTest() // ... ここまで長い前置き // StatusId: 200 を未読にする (search1, search2 両方に含まれる) - this.tabinfo.SetReadAllTab(200L, read: false); + this.tabinfo.SetReadAllTab(new TwitterStatusId("200"), read: false); Assert.Equal(1, tab1.UnreadCount); Assert.Equal(1, tab2.UnreadCount); // StatusId: 100 を未読にする (search1 のみに含まれる) - this.tabinfo.SetReadAllTab(100L, read: false); + this.tabinfo.SetReadAllTab(new TwitterStatusId("100"), read: false); Assert.Equal(2, tab1.UnreadCount); Assert.Equal(1, tab2.UnreadCount); } @@ -690,9 +690,9 @@ public void SetReadHomeTab_Test() // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読) homeTab.UnreadManage = true; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false }); - this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = false }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); @@ -713,9 +713,9 @@ public void SetReadHomeTab_ContainsReplyTest() // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読) // StatusId: 150 は未読だがリプライ属性が付いている homeTab.UnreadManage = true; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false }); - this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false, IsReply = true }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = false, IsReply = true }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = false }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); @@ -728,7 +728,7 @@ public void SetReadHomeTab_ContainsReplyTest() // リプライである StatusId: 150 を除いてすべて未読になっている Assert.Equal(1, homeTab.UnreadCount); - Assert.Equal(150L, homeTab.NextUnreadId); + Assert.Equal(new TwitterStatusId("150"), homeTab.NextUnreadId); } [Fact] @@ -738,14 +738,14 @@ public void SetReadHomeTab_ContainsFilterHitTest() // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読) homeTab.UnreadManage = true; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false }); - this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("150"), IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = false }); // StatusId: 150 だけ FilterTab の振り分けルールにヒットする (PostClass.FilterHit が true になる) var filterTab = new FilterTabModel("FilterTab"); filterTab.AddFilter(TestPostFilterRule.Create(x => - x.StatusId == 150L ? MyCommon.HITRESULT.Copy : MyCommon.HITRESULT.None)); + x.StatusId == new TwitterStatusId("150") ? MyCommon.HITRESULT.Copy : MyCommon.HITRESULT.None)); this.tabinfo.AddTab(filterTab); this.tabinfo.DistributePosts(); @@ -759,7 +759,7 @@ public void SetReadHomeTab_ContainsFilterHitTest() // FilterHit が true である StatusId: 150 を除いてすべて未読になっている Assert.Equal(1, homeTab.UnreadCount); - Assert.Equal(150L, homeTab.NextUnreadId); + Assert.Equal(new TwitterStatusId("150"), homeTab.NextUnreadId); } [Fact] @@ -767,13 +767,13 @@ public void SubmitUpdate_RemoveSubmit_Test() { var homeTab = this.tabinfo.HomeTab; - this.tabinfo.AddPost(new PostClass { StatusId = 100L }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100") }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); Assert.Equal(1, homeTab.AllCount); - this.tabinfo.RemovePostFromAllTabs(100L, setIsDeleted: true); + this.tabinfo.RemovePostFromAllTabs(new TwitterStatusId("100"), setIsDeleted: true); // この時点ではまだ削除されない Assert.Equal(1, homeTab.AllCount); @@ -782,7 +782,7 @@ public void SubmitUpdate_RemoveSubmit_Test() Assert.True(isDeletePost); Assert.Equal(0, homeTab.AllCount); - Assert.False(this.tabinfo.Posts.ContainsKey(100L)); + Assert.False(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); } [Fact] @@ -791,7 +791,7 @@ public void SubmitUpdate_RemoveSubmit_NotOrphaned_Test() var homeTab = this.tabinfo.HomeTab; var favTab = this.tabinfo.FavoriteTab; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsFav = true }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsFav = true }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); @@ -799,7 +799,7 @@ public void SubmitUpdate_RemoveSubmit_NotOrphaned_Test() Assert.Equal(1, favTab.AllCount); // favTab のみ発言を除去 (homeTab には残ったまま) - favTab.EnqueueRemovePost(100L, setIsDeleted: false); + favTab.EnqueueRemovePost(new TwitterStatusId("100"), setIsDeleted: false); // この時点ではまだ削除されない Assert.Equal(1, homeTab.AllCount); @@ -812,7 +812,7 @@ public void SubmitUpdate_RemoveSubmit_NotOrphaned_Test() Assert.Equal(0, favTab.AllCount); // homeTab には発言が残っているので Posts からは削除されない - Assert.True(this.tabinfo.Posts.ContainsKey(100L)); + Assert.True(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); } [Fact] @@ -834,13 +834,13 @@ public void SubmitUpdate_NotifyPriorityTest() dmTab.SoundFile = "dm.wav"; // 通常ツイート - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); // リプライ - this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsReply = true, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), IsReply = true, IsRead = false }); // DM - dmTab.AddPostQueue(new PostClass { StatusId = 300L, IsDm = true, IsRead = false }); + dmTab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("300"), IsDm = true, IsRead = false }); this.tabinfo.DistributePosts(); @@ -867,10 +867,10 @@ public void SubmitUpdate_IgnoreEmptySoundPath_Test() replyTab.SoundFile = ""; // 通常ツイート - this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); // リプライ - this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsReply = true, IsRead = false }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), IsReply = true, IsRead = false }); this.tabinfo.DistributePosts(); @@ -902,15 +902,15 @@ public void FilterAll_CopyFilterTest() myTab1.AddFilter(filter); myTab1.FilterModified = false; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" }); - this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "aaa" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "bbb" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // この時点での振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 100L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100") }, myTab1.StatusIds); // フィルタを変更する filter.FilterName = "bbb"; @@ -925,8 +925,8 @@ public void FilterAll_CopyFilterTest() // [statusId: 200] は Recent から MyTab1 にコピーされる // 変更後の振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 200L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("200") }, myTab1.StatusIds); } [Fact] @@ -948,15 +948,15 @@ public void FilterAll_CopyAndMarkFilterTest() myTab1.AddFilter(filter); myTab1.FilterModified = false; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" }); - this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "aaa" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "bbb" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // この時点での振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 100L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100") }, myTab1.StatusIds); // フィルタを変更する filter.FilterName = "bbb"; @@ -971,11 +971,11 @@ public void FilterAll_CopyAndMarkFilterTest() // [statusId: 200] は Recent から MyTab1 にコピーされ、マークが付与される // 変更後の振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 200L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("200") }, myTab1.StatusIds); // [statusId: 200] は IsMark が true の状態になる - Assert.True(this.tabinfo[200L]!.IsMark); + Assert.True(this.tabinfo[new TwitterStatusId("200")]!.IsMark); } [Fact] @@ -996,15 +996,15 @@ public void FilterAll_MoveFilterTest() myTab1.AddFilter(filter); myTab1.FilterModified = false; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" }); - this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "aaa" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "bbb" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // この時点での振り分け状態 - Assert.Equal(new[] { 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 100L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100") }, myTab1.StatusIds); // フィルタを変更する filter.FilterName = "bbb"; @@ -1019,8 +1019,8 @@ public void FilterAll_MoveFilterTest() // [statusId: 200] は Recent から MyTab1 に移動される // 変更後の振り分け状態 - Assert.Equal(new[] { 100L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 200L }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("200") }, myTab1.StatusIds); } [Fact] @@ -1053,16 +1053,16 @@ public void FilterAll_MoveFilterTest2() myTab2.AddFilter(filter2); myTab2.FilterModified = false; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" }); - this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "aaa" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "bbb" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // この時点での振り分け状態 - Assert.Equal(new[] { 300L }, homeTab.StatusIds); - Assert.Equal(new[] { 100L }, myTab1.StatusIds); - Assert.Equal(new[] { 200L }, myTab2.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("300") }, homeTab.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100") }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("200") }, myTab2.StatusIds); // MyTab1 のフィルタを変更する filter1.FilterName = "bbb"; @@ -1082,9 +1082,9 @@ public void FilterAll_MoveFilterTest2() // [statusId: 300] は Recent から MyTab2 に移動される // 変更後の振り分け状態 - Assert.Equal(new[] { 100L }, homeTab.StatusIds); - Assert.Equal(new[] { 200L }, myTab1.StatusIds); - Assert.Equal(new[] { 300L }, myTab2.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100") }, homeTab.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("200") }, myTab1.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("300") }, myTab2.StatusIds); } [Fact] @@ -1101,18 +1101,18 @@ public void FilterAll_ExcludeReplyFilterTest() replyTab.AddFilter(filter); replyTab.FilterModified = false; - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa", IsReply = true }); - this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb", IsReply = true }); - this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc", IsReply = true }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "aaa", IsReply = true }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "bbb", IsReply = true }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc", IsReply = true }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // この時点での振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 200L, 300L }, replyTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("200"), new TwitterStatusId("300") }, replyTab.StatusIds, AnyOrderComparer.Instance); // [statusId: 100] は IsExcludeReply が true の状態になっている - Assert.True(this.tabinfo[100L]!.IsExcludeReply); + Assert.True(this.tabinfo[new TwitterStatusId("100")]!.IsExcludeReply); // Reply のフィルタを変更する filter.ExFilterName = "bbb"; @@ -1127,28 +1127,28 @@ public void FilterAll_ExcludeReplyFilterTest() // [statusId: 200] は Reply から取り除かれ、IsExcludeReply が true になる // 変更後の振り分け状態 - Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer.Instance); - Assert.Equal(new[] { 100L, 300L }, replyTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("200"), new TwitterStatusId("300") }, homeTab.StatusIds, AnyOrderComparer.Instance); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("300") }, replyTab.StatusIds, AnyOrderComparer.Instance); // [statusId: 100] は IsExcludeReply が false の状態になる - Assert.False(this.tabinfo[100L]!.IsExcludeReply); + Assert.False(this.tabinfo[new TwitterStatusId("100")]!.IsExcludeReply); // [statusId: 200] は IsExcludeReply が true の状態になる - Assert.True(this.tabinfo[200L]!.IsExcludeReply); + Assert.True(this.tabinfo[new TwitterStatusId("200")]!.IsExcludeReply); } [Fact] public void ClearTabIds_InnerStorageTabTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); this.tabinfo.AddTab(tab); this.tabinfo.SubmitUpdate(); - Assert.True(tab.Contains(100L)); + Assert.True(tab.Contains(new TwitterStatusId("100"))); this.tabinfo.ClearTabIds("search"); - Assert.False(tab.Contains(100L)); + Assert.False(tab.Contains(new TwitterStatusId("100"))); } [Fact] @@ -1158,16 +1158,16 @@ public void ClearTabIds_FilterTab_MovedPost_OrphanedTest() filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); this.tabinfo.AddTab(filterTab); - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "opentween" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); - Assert.True(this.tabinfo.Posts.ContainsKey(100L)); - Assert.True(filterTab.Contains(100L)); + Assert.True(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); + Assert.True(filterTab.Contains(new TwitterStatusId("100"))); this.tabinfo.ClearTabIds("filter"); - Assert.False(this.tabinfo.Posts.ContainsKey(100L)); - Assert.False(filterTab.Contains(100L)); + Assert.False(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); + Assert.False(filterTab.Contains(new TwitterStatusId("100"))); } [Fact] @@ -1181,40 +1181,40 @@ public void ClearTabIds_FilterTab_MovedPost_NotOrphanedTest() filterTab2.AddFilter(new() { FilterName = "opentween", MoveMatches = true }); this.tabinfo.AddTab(filterTab2); - this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "opentween" }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); - Assert.True(this.tabinfo.Posts.ContainsKey(100L)); - Assert.True(filterTab1.Contains(100L)); - Assert.True(filterTab2.Contains(100L)); + Assert.True(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); + Assert.True(filterTab1.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab2.Contains(new TwitterStatusId("100"))); this.tabinfo.ClearTabIds("filter1"); // 他に MoveMatches で移動している振り分けタブが存在する場合は TabInformations.Posts から削除しない - Assert.True(this.tabinfo.ContainsKey(100L)); - Assert.False(filterTab1.Contains(100L)); - Assert.True(filterTab2.Contains(100L)); + Assert.True(this.tabinfo.ContainsKey(new TwitterStatusId("100"))); + Assert.False(filterTab1.Contains(new TwitterStatusId("100"))); + Assert.True(filterTab2.Contains(new TwitterStatusId("100"))); } [Fact] public void ClearTabIds_NotAffectToOtherTabs_Test() { var otherTab = new PublicSearchTabModel("search"); - otherTab.AddPostQueue(new PostClass { StatusId = 100L }); + otherTab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); this.tabinfo.AddTab(otherTab); - this.tabinfo.AddPost(new PostClass { StatusId = 100L }); + this.tabinfo.AddPost(new PostClass { StatusId = new TwitterStatusId("100") }); this.tabinfo.DistributePosts(); this.tabinfo.SubmitUpdate(); // Recent, search のタブに status_id = 100 の発言が存在する状態 - Assert.True(this.tabinfo.Posts.ContainsKey(100L)); - Assert.True(otherTab.Contains(100L)); + Assert.True(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); + Assert.True(otherTab.Contains(new TwitterStatusId("100"))); this.tabinfo.ClearTabIds("Recent"); - Assert.False(this.tabinfo.Posts.ContainsKey(100L)); - Assert.True(otherTab.Contains(100L)); + Assert.False(this.tabinfo.Posts.ContainsKey(new TwitterStatusId("100"))); + Assert.True(otherTab.Contains(new TwitterStatusId("100"))); } [Fact] @@ -1222,7 +1222,7 @@ public void RefreshOwl_HomeTabTest() { var post = new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), ScreenName = "aaa", UserId = 123L, IsOwl = true, @@ -1245,7 +1245,7 @@ public void RefreshOwl_InnerStoregeTabTest() var post = new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), ScreenName = "aaa", UserId = 123L, IsOwl = true, @@ -1265,7 +1265,7 @@ public void RefreshOwl_UnfollowedTest() { var post = new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), ScreenName = "aaa", UserId = 123L, IsOwl = false, diff --git a/OpenTween.Tests/Models/TabModelTest.cs b/OpenTween.Tests/Models/TabModelTest.cs index 26fceab07..f256d15aa 100644 --- a/OpenTween.Tests/Models/TabModelTest.cs +++ b/OpenTween.Tests/Models/TabModelTest.cs @@ -39,8 +39,8 @@ public void AnchorPost_Test() var posts = new[] { - new PostClass { StatusId = 100L }, - new PostClass { StatusId = 110L }, + new PostClass { StatusId = new TwitterStatusId("100") }, + new PostClass { StatusId = new TwitterStatusId("110") }, }; tab.AddPostQueue(posts[0]); tab.AddPostQueue(posts[1]); @@ -51,8 +51,8 @@ public void AnchorPost_Test() tab.AnchorPost = posts[1]; - Assert.Equal(110L, tab.AnchorStatusId); - Assert.Equal(110L, tab.AnchorPost.StatusId); + Assert.Equal(new TwitterStatusId("110"), tab.AnchorStatusId); + Assert.Equal(new TwitterStatusId("110"), tab.AnchorPost.StatusId); } [Fact] @@ -62,15 +62,15 @@ public void AnchorPost_DeletedTest() var posts = new[] { - new PostClass { StatusId = 100L }, + new PostClass { StatusId = new TwitterStatusId("100") }, }; tab.AddPostQueue(posts[0]); tab.AddSubmit(); tab.AnchorPost = posts[0]; - Assert.Equal(100L, tab.AnchorPost.StatusId); + Assert.Equal(new TwitterStatusId("100"), tab.AnchorPost.StatusId); - tab.EnqueueRemovePost(100L, setIsDeleted: true); + tab.EnqueueRemovePost(new TwitterStatusId("100"), setIsDeleted: true); tab.RemoveSubmit(); Assert.Null(tab.AnchorPost); @@ -83,13 +83,13 @@ public void ClearAnchor_Test() var posts = new[] { - new PostClass { StatusId = 100L }, + new PostClass { StatusId = new TwitterStatusId("100") }, }; tab.AddPostQueue(posts[0]); tab.AddSubmit(); tab.AnchorPost = posts[0]; - Assert.Equal(100L, tab.AnchorPost.StatusId); + Assert.Equal(new TwitterStatusId("100"), tab.AnchorPost.StatusId); tab.ClearAnchor(); Assert.Null(tab.AnchorPost); } @@ -101,9 +101,9 @@ public void SelectPosts_Test() var posts = new[] { - new PostClass { StatusId = 100L }, - new PostClass { StatusId = 110L }, - new PostClass { StatusId = 120L }, + new PostClass { StatusId = new TwitterStatusId("100") }, + new PostClass { StatusId = new TwitterStatusId("110") }, + new PostClass { StatusId = new TwitterStatusId("120") }, }; tab.AddPostQueue(posts[0]); tab.AddPostQueue(posts[1]); @@ -112,8 +112,8 @@ public void SelectPosts_Test() tab.SelectPosts(new[] { 0, 2 }); - Assert.Equal(new[] { 100L, 120L }, tab.SelectedStatusIds); - Assert.Equal(100L, tab.SelectedStatusId); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("120") }, tab.SelectedStatusIds); + Assert.Equal(new TwitterStatusId("100"), tab.SelectedStatusId); Assert.Equal(new[] { posts[0], posts[2] }, tab.SelectedPosts); Assert.Equal(posts[0], tab.SelectedPost); Assert.Equal(0, tab.SelectedIndex); @@ -123,7 +123,7 @@ public void SelectPosts_Test() public void SelectPosts_EmptyTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); tab.AddSubmit(); tab.SelectPosts(Array.Empty()); @@ -139,7 +139,7 @@ public void SelectPosts_EmptyTest() public void SelectPosts_InvalidIndexTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); tab.AddSubmit(); Assert.Throws(() => tab.SelectPosts(new[] { -1 })); @@ -154,16 +154,16 @@ public void EnqueueRemovePost_Test() UnreadManage = true, }; - tab.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 110L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 120L, IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("110"), IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("120"), IsRead = false }); tab.AddSubmit(); Assert.Equal(3, tab.AllCount); Assert.Equal(3, tab.UnreadCount); - tab.EnqueueRemovePost(100L, setIsDeleted: false); + tab.EnqueueRemovePost(new TwitterStatusId("100"), setIsDeleted: false); // この時点では削除は行われない Assert.Equal(3, tab.AllCount); @@ -173,8 +173,8 @@ public void EnqueueRemovePost_Test() Assert.Equal(2, tab.AllCount); Assert.Equal(2, tab.UnreadCount); - Assert.Equal(new[] { 110L, 120L }, tab.StatusIds); - Assert.Equal(new[] { 100L }, removedIds.AsEnumerable()); + Assert.Equal(new[] { new TwitterStatusId("110"), new TwitterStatusId("120") }, tab.StatusIds); + Assert.Equal(new[] { new TwitterStatusId("100") }, removedIds.AsEnumerable()); } [Fact] @@ -185,14 +185,14 @@ public void EnqueueRemovePost_SetIsDeletedTest() UnreadManage = true, }; - var post = new PostClass { StatusId = 100L, IsRead = false }; + var post = new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }; tab.AddPostQueue(post); tab.AddSubmit(); Assert.Equal(1, tab.AllCount); Assert.Equal(1, tab.UnreadCount); - tab.EnqueueRemovePost(100L, setIsDeleted: true); + tab.EnqueueRemovePost(new TwitterStatusId("100"), setIsDeleted: true); // この時点ではタブからの削除は行われないが、PostClass.IsDeleted は true にセットされる Assert.Equal(1, tab.AllCount); @@ -203,7 +203,7 @@ public void EnqueueRemovePost_SetIsDeletedTest() Assert.Equal(0, tab.AllCount); Assert.Equal(0, tab.UnreadCount); - Assert.Equal(new[] { 100L }, removedIds.AsEnumerable()); + Assert.Equal(new[] { new TwitterStatusId("100") }, removedIds.AsEnumerable()); } [Fact] @@ -214,14 +214,14 @@ public void EnqueueRemovePost_UnknownIdTest() UnreadManage = true, }; - tab.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); tab.AddSubmit(); Assert.Equal(1, tab.AllCount); Assert.Equal(1, tab.UnreadCount); - // StatusId = 999L は存在しない - tab.EnqueueRemovePost(999L, setIsDeleted: false); + // StatusId = 999 は存在しない + tab.EnqueueRemovePost(new TwitterStatusId("999"), setIsDeleted: false); Assert.Equal(1, tab.AllCount); Assert.Equal(1, tab.UnreadCount); @@ -237,49 +237,49 @@ public void EnqueueRemovePost_UnknownIdTest() public void EnqueueRemovePost_SelectedTest() { var tab = new PublicSearchTabModel("search"); - tab.AddPostQueue(new PostClass { StatusId = 100L }); - tab.AddPostQueue(new PostClass { StatusId = 110L }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100") }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("110") }); tab.AddSubmit(); tab.SelectPosts(new[] { 0, 1 }); Assert.Equal(2, tab.AllCount); - Assert.Equal(new[] { 100L, 110L }, tab.SelectedStatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("110") }, tab.SelectedStatusIds); - tab.EnqueueRemovePost(100L, setIsDeleted: false); + tab.EnqueueRemovePost(new TwitterStatusId("100"), setIsDeleted: false); // この時点では変化しない Assert.Equal(2, tab.AllCount); - Assert.Equal(new[] { 100L, 110L }, tab.SelectedStatusIds); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("110") }, tab.SelectedStatusIds); tab.RemoveSubmit(); // 削除された発言の選択が解除される Assert.Equal(1, tab.AllCount); - Assert.Equal(new[] { 110L }, tab.SelectedStatusIds); + Assert.Equal(new[] { new TwitterStatusId("110") }, tab.SelectedStatusIds); } [Fact] public void ReplacePost_SuccessTest() { var tab = new PublicSearchTabModel("search"); - var origPost = new PostClass { StatusId = 100L }; + var origPost = new PostClass { StatusId = new TwitterStatusId("100") }; tab.AddPostQueue(origPost); tab.AddSubmit(); - Assert.Same(origPost, tab.Posts[100L]); + Assert.Same(origPost, tab.Posts[new TwitterStatusId("100")]); - var newPost = new PostClass { StatusId = 100L, InReplyToStatusId = 200L }; + var newPost = new PostClass { StatusId = new TwitterStatusId("100"), InReplyToStatusId = new TwitterStatusId("200") }; Assert.True(tab.ReplacePost(newPost)); - Assert.Same(newPost, tab.Posts[100L]); + Assert.Same(newPost, tab.Posts[new TwitterStatusId("100")]); } [Fact] public void ReplacePost_FailedTest() { var tab = new PublicSearchTabModel("search"); - Assert.False(tab.Contains(100L)); + Assert.False(tab.Contains(new TwitterStatusId("100"))); - var newPost = new PostClass { StatusId = 100L, InReplyToStatusId = 200L }; + var newPost = new PostClass { StatusId = new TwitterStatusId("100"), InReplyToStatusId = new TwitterStatusId("200") }; Assert.False(tab.ReplacePost(newPost)); } @@ -291,25 +291,25 @@ public void NextUnreadId_Test() tab.UnreadManage = true; // 未読なし - Assert.Equal(-1L, tab.NextUnreadId); + Assert.Null(tab.NextUnreadId); tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = false, // 未読 }); tab.AddSubmit(); - Assert.Equal(100L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("100"), tab.NextUnreadId); tab.AddPostQueue(new PostClass { - StatusId = 50L, + StatusId = new TwitterStatusId("50"), IsRead = true, // 既読 }); tab.AddSubmit(); - Assert.Equal(100L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("100"), tab.NextUnreadId); } [Fact] @@ -322,12 +322,12 @@ public void NextUnreadId_DisabledTest() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = false, // 未読 }); tab.AddSubmit(); - Assert.Equal(-1L, tab.NextUnreadId); + Assert.Null(tab.NextUnreadId); } [Fact] @@ -343,26 +343,26 @@ public void NextUnreadId_SortByIdAscTest() // 画面には上から 100 → 200 → 300 の順に並ぶ tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = false, }); tab.AddPostQueue(new() { - StatusId = 200L, + StatusId = new TwitterStatusId("200"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), IsRead = false, }); tab.AddPostQueue(new() { - StatusId = 300L, + StatusId = new TwitterStatusId("300"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), IsRead = false, }); tab.AddSubmit(); // 昇順/降順に関わらず、CreatedAtForSorting の小さい順に未読の ID を返す - Assert.Equal(100L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("100"), tab.NextUnreadId); } [Fact] @@ -378,26 +378,26 @@ public void NextUnreadId_SortByIdDescTest() // 画面には上から 300 → 200 → 100 の順に並ぶ tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = false, }); tab.AddPostQueue(new() { - StatusId = 200L, + StatusId = new TwitterStatusId("200"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), IsRead = false, }); tab.AddPostQueue(new() { - StatusId = 300L, + StatusId = new TwitterStatusId("300"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), IsRead = false, }); tab.AddSubmit(); // 昇順/降順に関わらず、CreatedAtForSorting の小さい順に未読の ID を返す - Assert.Equal(100L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("100"), tab.NextUnreadId); } [Fact] @@ -411,13 +411,13 @@ public void NextUnreadId_SortByScreenNameAscTest() tab.SetSortMode(ComparerMode.Name, SortOrder.Ascending); // 画面には上から 200 → 100 → 300 の順に並ぶ - tab.AddPostQueue(new PostClass { StatusId = 100L, ScreenName = "bbb", IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 200L, ScreenName = "aaa", IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 300L, ScreenName = "ccc", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "bbb", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "aaa", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc", IsRead = false }); tab.AddSubmit(); // 昇順/降順に関わらず、ScreenName の辞書順で小さい順に未読の ID を返す - Assert.Equal(200L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("200"), tab.NextUnreadId); } [Fact] @@ -431,13 +431,13 @@ public void NextUnreadId_SortByScreenNameDescTest() tab.SetSortMode(ComparerMode.Name, SortOrder.Descending); // 画面には上から 300 → 100 → 200 の順に並ぶ - tab.AddPostQueue(new PostClass { StatusId = 100L, ScreenName = "bbb", IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 200L, ScreenName = "aaa", IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 300L, ScreenName = "ccc", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), ScreenName = "bbb", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), ScreenName = "aaa", IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("300"), ScreenName = "ccc", IsRead = false }); tab.AddSubmit(); // 昇順/降順に関わらず、ScreenName の辞書順で小さい順に未読の ID を返す - Assert.Equal(200L, tab.NextUnreadId); + Assert.Equal(new TwitterStatusId("200"), tab.NextUnreadId); } [Fact] @@ -452,7 +452,7 @@ public void UnreadCount_Test() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = false, // 未読 }); tab.AddSubmit(); @@ -461,7 +461,7 @@ public void UnreadCount_Test() tab.AddPostQueue(new PostClass { - StatusId = 50L, + StatusId = new TwitterStatusId("50"), IsRead = true, // 既読 }); tab.AddSubmit(); @@ -479,7 +479,7 @@ public void UnreadCount_DisabledTest() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = false, // 未読 }); tab.AddSubmit(); @@ -500,19 +500,19 @@ public void NextUnreadIndex_Test() tab.AddPostQueue(new PostClass { - StatusId = 50L, + StatusId = new TwitterStatusId("50"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = true, // 既読 }); tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), IsRead = false, // 未読 }); tab.AddPostQueue(new PostClass { - StatusId = 150L, + StatusId = new TwitterStatusId("150"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), IsRead = false, // 未読 }); @@ -532,7 +532,7 @@ public void NextUnreadIndex_DisabledTest() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), IsRead = false, // 未読 }); @@ -549,13 +549,13 @@ public void GetUnreadIds_Test() Assert.Empty(tab.GetUnreadIds()); - tab.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false }); - tab.AddPostQueue(new PostClass { StatusId = 200L, IsRead = true }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("100"), IsRead = false }); + tab.AddPostQueue(new PostClass { StatusId = new TwitterStatusId("200"), IsRead = true }); tab.AddSubmit(); - Assert.Equal(new[] { 100L }, tab.GetUnreadIds()); + Assert.Equal(new[] { new TwitterStatusId("100") }, tab.GetUnreadIds()); - tab.SetReadState(100L, true); // 既読にする + tab.SetReadState(new TwitterStatusId("100"), true); // 既読にする Assert.Empty(tab.GetUnreadIds()); } @@ -569,14 +569,14 @@ public void SetReadState_MarkAsReadTest() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = false, // 未読 }); tab.AddSubmit(); Assert.Equal(1, tab.UnreadCount); - tab.SetReadState(100L, true); // 既読にする + tab.SetReadState(new TwitterStatusId("100"), true); // 既読にする Assert.Equal(0, tab.UnreadCount); } @@ -590,14 +590,14 @@ public void SetReadState_MarkAsUnreadTest() tab.AddPostQueue(new PostClass { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), IsRead = true, // 既読 }); tab.AddSubmit(); Assert.Equal(0, tab.UnreadCount); - tab.SetReadState(100L, false); // 未読にする + tab.SetReadState(new TwitterStatusId("100"), false); // 未読にする Assert.Equal(1, tab.UnreadCount); } @@ -680,7 +680,7 @@ public void SearchPostsAll_Test() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", ScreenName = "", @@ -688,7 +688,7 @@ public void SearchPostsAll_Test() }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", ScreenName = "", @@ -696,7 +696,7 @@ public void SearchPostsAll_Test() }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", ScreenName = "", @@ -704,7 +704,7 @@ public void SearchPostsAll_Test() }); tab.AddPostQueue(new() { - StatusId = 130L, + StatusId = new TwitterStatusId("130"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), // 3 TextFromApi = "abc", ScreenName = "", @@ -712,7 +712,7 @@ public void SearchPostsAll_Test() }); tab.AddPostQueue(new() { - StatusId = 140L, + StatusId = new TwitterStatusId("140"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 4), // 4 TextFromApi = "def", ScreenName = "", @@ -738,7 +738,7 @@ public void SearchPostsAll_ReverseOrderTest() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", ScreenName = "", @@ -746,7 +746,7 @@ public void SearchPostsAll_ReverseOrderTest() }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", ScreenName = "", @@ -754,7 +754,7 @@ public void SearchPostsAll_ReverseOrderTest() }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", ScreenName = "", @@ -762,7 +762,7 @@ public void SearchPostsAll_ReverseOrderTest() }); tab.AddPostQueue(new() { - StatusId = 130L, + StatusId = new TwitterStatusId("130"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 3), // 3 TextFromApi = "abc", ScreenName = "", @@ -770,7 +770,7 @@ public void SearchPostsAll_ReverseOrderTest() }); tab.AddPostQueue(new() { - StatusId = 140L, + StatusId = new TwitterStatusId("140"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 4), // 4 TextFromApi = "def", ScreenName = "", @@ -796,19 +796,19 @@ public void GetterSingle_Test() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", }); @@ -816,8 +816,8 @@ public void GetterSingle_Test() tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); - Assert.Equal(100L, tab[0].StatusId); - Assert.Equal(120L, tab[2].StatusId); + Assert.Equal(new TwitterStatusId("100"), tab[0].StatusId); + Assert.Equal(new TwitterStatusId("120"), tab[2].StatusId); } [Fact] @@ -827,19 +827,19 @@ public void GetterSingle_ErrorTest() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", }); @@ -858,19 +858,19 @@ public void GetterSlice_Test() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", }); @@ -878,9 +878,9 @@ public void GetterSlice_Test() tab.SetSortMode(ComparerMode.Id, SortOrder.Ascending); tab.AddSubmit(); - Assert.Equal(new[] { 100L, 110L, 120L }, tab[0, 2].Select(x => x.StatusId)); - Assert.Equal(new[] { 100L }, tab[0, 0].Select(x => x.StatusId)); - Assert.Equal(new[] { 120L }, tab[2, 2].Select(x => x.StatusId)); + Assert.Equal(new[] { new TwitterStatusId("100"), new TwitterStatusId("110"), new TwitterStatusId("120") }, tab[0, 2].Select(x => x.StatusId)); + Assert.Equal(new[] { new TwitterStatusId("100") }, tab[0, 0].Select(x => x.StatusId)); + Assert.Equal(new[] { new TwitterStatusId("120") }, tab[2, 2].Select(x => x.StatusId)); } [Fact] @@ -890,19 +890,19 @@ public void GetterSlice_ErrorTest() tab.AddPostQueue(new() { - StatusId = 100L, + StatusId = new TwitterStatusId("100"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 0), // 0 TextFromApi = "abcd", }); tab.AddPostQueue(new() { - StatusId = 110L, + StatusId = new TwitterStatusId("110"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 1), // 1 TextFromApi = "efgh", }); tab.AddPostQueue(new() { - StatusId = 120L, + StatusId = new TwitterStatusId("120"), CreatedAtForSorting = new(2023, 1, 1, 0, 0, 2), // 2 TextFromApi = "ijkl", }); diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index c2f7a9861..38124eb76 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -79,7 +79,7 @@ public void CreateFromStatus_Test() var status = this.CreateStatus(); var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet); - Assert.Equal(status.Id, post.StatusId); + Assert.Equal(new TwitterStatusId(status.IdStr), post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); Assert.Equal("hoge", post.Text); Assert.Equal("hoge", post.TextFromApi); @@ -164,9 +164,9 @@ public void CreateFromStatus_RetweetTest() var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); - Assert.Equal(retweetStatus.Id, post.StatusId); + Assert.Equal(new TwitterStatusId(retweetStatus.IdStr), post.StatusId); Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId); - Assert.Equal(originalStatus.Id, post.RetweetedId); + Assert.Equal(new TwitterStatusId(originalStatus.IdStr), post.RetweetedId); Assert.Equal(originalStatus.User.Id, post.UserId); Assert.Equal("OpenTween", post.Source); @@ -228,7 +228,7 @@ public void CreateFromDirectMessageEvent_Test() var apps = this.CreateApps(); var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); - Assert.Equal(long.Parse(eventItem.Id), post.StatusId); + Assert.Equal(new TwitterDirectMessageId(eventItem.Id), post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); Assert.Equal("hoge", post.Text); Assert.Equal("hoge", post.TextFromApi); diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index e6860d72e..d94bada41 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -189,11 +189,11 @@ public void GetReadableVersionTest(string fileVersion, string expected) public static readonly TheoryData GetStatusUrlTest1TestCase = new() { { - new PostClass { StatusId = 249493863826350080L, ScreenName = "Favstar_LM", RetweetedId = null, RetweetedBy = null }, + new PostClass { StatusId = new TwitterStatusId("249493863826350080"), ScreenName = "Favstar_LM", RetweetedId = null, RetweetedBy = null }, "https://twitter.com/Favstar_LM/status/249493863826350080" }, { - new PostClass { StatusId = 216033842434289664L, ScreenName = "haru067", RetweetedId = 200245741443235840L, RetweetedBy = "re4k" }, + new PostClass { StatusId = new TwitterStatusId("216033842434289664"), ScreenName = "haru067", RetweetedId = new TwitterStatusId("200245741443235840"), RetweetedBy = "re4k" }, "https://twitter.com/haru067/status/200245741443235840" }, }; @@ -207,7 +207,7 @@ public void GetStatusUrlTest1(PostClass post, string expected) [InlineData("Favstar_LM", 249493863826350080L, "https://twitter.com/Favstar_LM/status/249493863826350080")] [InlineData("haru067", 200245741443235840L, "https://twitter.com/haru067/status/200245741443235840")] public void GetStatusUrlTest2(string screenName, long statusId, string expected) - => Assert.Equal(expected, MyCommon.GetStatusUrl(screenName, statusId)); + => Assert.Equal(expected, MyCommon.GetStatusUrl(screenName, new TwitterStatusId(statusId))); [Fact] public void GetErrorLogPathTest() diff --git a/OpenTween.Tests/TimelineListViewCacheTest.cs b/OpenTween.Tests/TimelineListViewCacheTest.cs index 0e6f142a2..6b88e5d2c 100644 --- a/OpenTween.Tests/TimelineListViewCacheTest.cs +++ b/OpenTween.Tests/TimelineListViewCacheTest.cs @@ -34,7 +34,7 @@ private PostClass CreatePost() { return new() { - StatusId = this.random.Next(10000), + StatusId = new TwitterStatusId(this.random.Next(10000)), UserId = this.random.Next(10000), ScreenName = "test", Nickname = "てすと", @@ -153,7 +153,7 @@ public void GetItem_RetweetTest() var post = this.CreatePost() with { - RetweetedId = 50L, + RetweetedId = new TwitterStatusId("50"), RetweetedBy = "hoge", }; @@ -317,7 +317,7 @@ public void GetStyle_ForeColor_RetweetTest() var post = this.CreatePost() with { - RetweetedId = 100L, + RetweetedId = new TwitterStatusId("100"), }; tab.AddPostQueue(post); diff --git a/OpenTween.Tests/TweetDetailsViewTest.cs b/OpenTween.Tests/TweetDetailsViewTest.cs index 5164b2ff4..babbe42b0 100644 --- a/OpenTween.Tests/TweetDetailsViewTest.cs +++ b/OpenTween.Tests/TweetDetailsViewTest.cs @@ -37,7 +37,7 @@ public void FormatQuoteTweetHtml_PostClassTest() { var post = new PostClass { - StatusId = 12345L, + StatusId = new TwitterStatusId("12345"), Nickname = "upsilon", ScreenName = "kim_upsilon", Text = "@twitterapi hogehoge", @@ -57,7 +57,7 @@ public void FormatQuoteTweetHtml_PostClassTest() [Fact] public void FormatQuoteTweetHtml_HtmlTest() { - var statusId = 12345L; // リンク先のステータスID + var statusId = new TwitterStatusId("12345"); // リンク先のステータスID var html = "hogehoge"; // HTMLをそのまま出力する (エスケープしない) var expected = "" + @@ -73,7 +73,7 @@ public void FormatQuoteTweetHtml_ReplyHtmlTest() var expected = "" + "
hogehoge
" + "
"; - Assert.Equal(expected, TweetDetailsView.FormatQuoteTweetHtml(12345L, "hogehoge", isReply: true)); + Assert.Equal(expected, TweetDetailsView.FormatQuoteTweetHtml(new TwitterStatusId("12345"), "hogehoge", isReply: true)); } [Fact] diff --git a/OpenTween.Tests/TwitterTest.cs b/OpenTween.Tests/TwitterTest.cs index 2c03f9698..de1cd17c7 100644 --- a/OpenTween.Tests/TwitterTest.cs +++ b/OpenTween.Tests/TwitterTest.cs @@ -86,26 +86,26 @@ public void ThirdPartyStatusUrlRegexTest(string url, string[] expected) [Fact] public void FindTopOfReplyChainTest() { - var posts = new Dictionary + var posts = new Dictionary { - [950L] = new PostClass { StatusId = 950L, InReplyToStatusId = null }, // このツイートが末端 - [987L] = new PostClass { StatusId = 987L, InReplyToStatusId = 950L }, - [999L] = new PostClass { StatusId = 999L, InReplyToStatusId = 987L }, - [1000L] = new PostClass { StatusId = 1000L, InReplyToStatusId = 999L }, + [new TwitterStatusId("950")] = new PostClass { StatusId = new TwitterStatusId("950"), InReplyToStatusId = null }, // このツイートが末端 + [new TwitterStatusId("987")] = new PostClass { StatusId = new TwitterStatusId("987"), InReplyToStatusId = new TwitterStatusId("950") }, + [new TwitterStatusId("999")] = new PostClass { StatusId = new TwitterStatusId("999"), InReplyToStatusId = new TwitterStatusId("987") }, + [new TwitterStatusId("1000")] = new PostClass { StatusId = new TwitterStatusId("1000"), InReplyToStatusId = new TwitterStatusId("999") }, }; - Assert.Equal(950L, Twitter.FindTopOfReplyChain(posts, 1000L).StatusId); - Assert.Equal(950L, Twitter.FindTopOfReplyChain(posts, 950L).StatusId); - Assert.Throws(() => Twitter.FindTopOfReplyChain(posts, 500L)); + Assert.Equal(new TwitterStatusId("950"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1000")).StatusId); + Assert.Equal(new TwitterStatusId("950"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("950")).StatusId); + Assert.Throws(() => Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("500"))); - posts = new Dictionary + posts = new Dictionary { - // 1200L は posts の中に存在しない - [1210L] = new PostClass { StatusId = 1210L, InReplyToStatusId = 1200L }, - [1220L] = new PostClass { StatusId = 1220L, InReplyToStatusId = 1210L }, - [1230L] = new PostClass { StatusId = 1230L, InReplyToStatusId = 1220L }, + // new TwitterStatusId("1200") は posts の中に存在しない + [new TwitterStatusId("1210")] = new PostClass { StatusId = new TwitterStatusId("1210"), InReplyToStatusId = new TwitterStatusId("1200") }, + [new TwitterStatusId("1220")] = new PostClass { StatusId = new TwitterStatusId("1220"), InReplyToStatusId = new TwitterStatusId("1210") }, + [new TwitterStatusId("1230")] = new PostClass { StatusId = new TwitterStatusId("1230"), InReplyToStatusId = new TwitterStatusId("1220") }, }; - Assert.Equal(1210L, Twitter.FindTopOfReplyChain(posts, 1230L).StatusId); - Assert.Equal(1210L, Twitter.FindTopOfReplyChain(posts, 1210L).StatusId); + Assert.Equal(new TwitterStatusId("1210"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1230")).StatusId); + Assert.Equal(new TwitterStatusId("1210"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1210")).StatusId); } [Fact] diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 3cca489fe..e4df794b6 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -30,6 +30,7 @@ using System.Threading.Tasks; using OpenTween.Api.DataModel; using OpenTween.Connection; +using OpenTween.Models; namespace OpenTween.Api { @@ -136,12 +137,12 @@ public Task StatusesUserTimeline(string screenName, int? count return this.Connection.GetAsync(endpoint, param, "/statuses/user_timeline"); } - public Task StatusesShow(long statusId) + public Task StatusesShow(TwitterStatusId statusId) { var endpoint = new Uri("statuses/show.json", UriKind.Relative); var param = new Dictionary { - ["id"] = statusId.ToString(), + ["id"] = statusId.Id, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", @@ -166,7 +167,7 @@ public Task StatusesLookup(IReadOnlyList statusIds) public Task> StatusesUpdate( string status, - long? replyToId, + TwitterStatusId? replyToId, IReadOnlyList? mediaIds, bool? autoPopulateReplyMetadata = null, IReadOnlyList? excludeReplyUserIds = null, @@ -182,7 +183,7 @@ public Task> StatusesUpdate( }; if (replyToId != null) - param["in_reply_to_status_id"] = replyToId.ToString(); + param["in_reply_to_status_id"] = replyToId.Id; if (mediaIds != null && mediaIds.Count > 0) param.Add("media_ids", string.Join(",", mediaIds)); if (autoPopulateReplyMetadata != null) @@ -195,23 +196,23 @@ public Task> StatusesUpdate( return this.Connection.PostLazyAsync(endpoint, param); } - public Task> StatusesDestroy(long statusId) + public Task> StatusesDestroy(TwitterStatusId statusId) { var endpoint = new Uri("statuses/destroy.json", UriKind.Relative); var param = new Dictionary { - ["id"] = statusId.ToString(), + ["id"] = statusId.Id, }; return this.Connection.PostLazyAsync(endpoint, param); } - public Task> StatusesRetweet(long statusId) + public Task> StatusesRetweet(TwitterStatusId statusId) { var endpoint = new Uri("statuses/retweet.json", UriKind.Relative); var param = new Dictionary { - ["id"] = statusId.ToString(), + ["id"] = statusId.Id, ["include_entities"] = "true", ["include_ext_alt_text"] = "true", ["tweet_mode"] = "extended", @@ -470,12 +471,12 @@ public Task> DirectMessagesEventsNew(long re return this.Connection.PostJsonAsync(endpoint, json); } - public Task DirectMessagesEventsDestroy(string eventId) + public Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) { var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative); var param = new Dictionary { - ["id"] = eventId.ToString(), + ["id"] = eventId.Id, }; // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様 @@ -544,24 +545,24 @@ public Task FavoritesList(int? count = null, long? maxId = null return this.Connection.GetAsync(endpoint, param, "/favorites/list"); } - public Task> FavoritesCreate(long statusId) + public Task> FavoritesCreate(TwitterStatusId statusId) { var endpoint = new Uri("favorites/create.json", UriKind.Relative); var param = new Dictionary { - ["id"] = statusId.ToString(), + ["id"] = statusId.Id, ["tweet_mode"] = "extended", }; return this.Connection.PostLazyAsync(endpoint, param); } - public Task> FavoritesDestroy(long statusId) + public Task> FavoritesDestroy(TwitterStatusId statusId) { var endpoint = new Uri("favorites/destroy.json", UriKind.Relative); var param = new Dictionary { - ["id"] = statusId.ToString(), + ["id"] = statusId.Id, ["tweet_mode"] = "extended", }; diff --git a/OpenTween/Models/InternalStorageTabModel.cs b/OpenTween/Models/InternalStorageTabModel.cs index 81ee24140..3e23e9277 100644 --- a/OpenTween/Models/InternalStorageTabModel.cs +++ b/OpenTween/Models/InternalStorageTabModel.cs @@ -38,9 +38,9 @@ namespace OpenTween.Models { public abstract class InternalStorageTabModel : TabModel { - protected readonly ConcurrentDictionary internalPosts = new(); + protected readonly ConcurrentDictionary internalPosts = new(); - public override ConcurrentDictionary Posts + public override ConcurrentDictionary Posts => this.internalPosts; protected InternalStorageTabModel(string tabName) @@ -58,7 +58,7 @@ public override void AddPostQueue(PostClass post) base.AddPostQueue(post); } - public override void EnqueueRemovePost(long statusId, bool setIsDeleted) + public override void EnqueueRemovePost(PostId statusId, bool setIsDeleted) { base.EnqueueRemovePost(statusId, setIsDeleted); @@ -69,7 +69,7 @@ public override void EnqueueRemovePost(long statusId, bool setIsDeleted) } } - public override bool RemovePostImmediately(long statusId) + public override bool RemovePostImmediately(PostId statusId) { if (!base.RemovePostImmediately(statusId)) return false; @@ -85,7 +85,7 @@ public override void ClearIDs() this.internalPosts.Clear(); } - internal override bool SetReadState(long statusId, bool read) + internal override bool SetReadState(PostId statusId, bool read) { if (this.Posts.TryGetValue(statusId, out var post)) post.IsRead = read; diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index ca5bc312a..3015a7c47 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -65,7 +65,7 @@ double Latitude /// public DateTimeUtc CreatedAtForSorting { get; init; } - public long StatusId { get; init; } + public PostId StatusId { get; init; } = null!; public string Text { @@ -103,7 +103,7 @@ public string Text public string? RetweetedBy { get; init; } - public long? RetweetedId { get; init; } + public PostId? RetweetedId { get; init; } public long? RetweetedByUserId { get; init; } @@ -111,7 +111,7 @@ public string Text public List Media { get; init; } = new(); - public long[] QuoteStatusIds { get; init; } = Array.Empty(); + public PostId[] QuoteStatusIds { get; init; } = Array.Empty(); public ExpandedUrlInfo[] ExpandedUrls { get; init; } = Array.Empty(); @@ -203,7 +203,7 @@ public int StateIndex public string TextSingleLine => this.TextFromApi.Replace("\n", " "); - public long? InReplyToStatusId { get; init; } + public PostId? InReplyToStatusId { get; init; } public bool IsProtect { get; init; } @@ -291,7 +291,7 @@ public PostClass ConvertToOriginalPost() var originalPost = this with { - StatusId = this.RetweetedId.Value, + StatusId = this.RetweetedId, CreatedAtForSorting = this.CreatedAt, RetweetedId = null, RetweetedBy = "", diff --git a/OpenTween/Models/PostId.cs b/OpenTween/Models/PostId.cs new file mode 100644 index 000000000..82b36654a --- /dev/null +++ b/OpenTween/Models/PostId.cs @@ -0,0 +1,68 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTween.Models +{ + [DebuggerDisplay("{IdType}:{Id}")] + public abstract class PostId + : IEquatable, IComparable + { + public abstract string IdType { get; } + + public abstract string Id { get; } + + public virtual int CompareTo(PostId other) + { + var compareByIdType = this.IdType.CompareTo(other.IdType); + if (compareByIdType != 0) + return compareByIdType; + + return this.Id.CompareTo(other.Id); + } + + public virtual bool Equals(PostId other) + => this.IdType == other.IdType && this.Id == other.Id; + + public override bool Equals(object obj) + => obj is PostId otherId && this.Equals(otherId); + + public override int GetHashCode() + => this.IdType.GetHashCode() ^ this.Id.GetHashCode(); + + public override string ToString() + => this.Id; + + public static bool operator ==(PostId? left, PostId? right) + => EqualityComparer.Default.Equals(left, right); + + public static bool operator !=(PostId? left, PostId? right) + => !EqualityComparer.Default.Equals(left, right); + } +} diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index b41312a10..eee786929 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -47,10 +47,10 @@ public IReadOnlyTabCollection Tabs public MuteTabModel MuteTab { get; private set; } = new(); - public ConcurrentDictionary Posts { get; } = new(); + public ConcurrentDictionary Posts { get; } = new(); - private readonly Dictionary quotes = new(); - private readonly ConcurrentDictionary retweetsCount = new(); + private readonly Dictionary quotes = new(); + private readonly ConcurrentDictionary retweetsCount = new(); public Stack RemovedTab { get; } = new(); @@ -62,7 +62,7 @@ public IReadOnlyTabCollection Tabs // AddPost(複数回) -> DistributePosts -> SubmitUpdate private readonly TabCollection tabs = new(); - private readonly ConcurrentQueue addQueue = new(); + private readonly ConcurrentQueue addQueue = new(); /// 通知サウンドを再生する優先順位 private readonly Dictionary notifyPriorityByTabType = new() @@ -156,7 +156,6 @@ public void RemoveTab(string tabName) { var exist = false; var id = tb.GetStatusIdAt(idx); - if (id < 0) continue; foreach (var tab in this.Tabs) { if (tab != tb && tab != dmTab) @@ -383,7 +382,7 @@ public SortOrder ToggleSortOrder(ComparerMode sortMode) return this.SortOrder; } - public void RemovePostFromAllTabs(long statusId, bool setIsDeleted) + public void RemovePostFromAllTabs(PostId statusId, bool setIsDeleted) { foreach (var tab in this.Tabs) { @@ -415,7 +414,7 @@ public int SubmitUpdate( isDeletePost = false; var addedCountTotal = 0; - var removedIdsAll = new List(); + var removedIdsAll = new List(); var notifyPostsList = new List(); var currentNotifyPriority = -1; @@ -647,7 +646,7 @@ private int UpdateRetweetCount(PostClass retweetPost) if (retweetPost.RetweetedId == null) throw new InvalidOperationException(); - var retweetedId = retweetPost.RetweetedId.Value; + var retweetedId = retweetPost.RetweetedId; return this.retweetsCount.AddOrUpdate(retweetedId, 1, (k, v) => v >= 10 ? 1 : v + 1); } @@ -670,7 +669,7 @@ public bool AddQuoteTweet(PostClass item) /// 変更するツイートのID /// 既読状態 /// 既読状態に変化があれば true、変化がなければ false - public bool SetReadAllTab(long statusId, bool read) + public bool SetReadAllTab(PostId statusId, bool read) { lock (this.lockObj) { @@ -713,7 +712,7 @@ public void SetReadHomeTab() } } - public PostClass? this[long id] + public PostClass? this[PostId id] { get { @@ -729,7 +728,7 @@ public PostClass? this[long id] } } - public bool ContainsKey(long id) + public bool ContainsKey(PostId id) { // DM,公式検索は非対応 lock (this.lockObj) @@ -759,7 +758,7 @@ public void FilterAll() lock (this.lockObj) { var homeTab = this.HomeTab; - var detachedIdsAll = Enumerable.Empty(); + var detachedIdsAll = Enumerable.Empty(); foreach (var tab in this.Tabs.OfType().ToArray()) { diff --git a/OpenTween/Models/TabModel.cs b/OpenTween/Models/TabModel.cs index 5dd49f52f..11498f1d5 100644 --- a/OpenTween/Models/TabModel.cs +++ b/OpenTween/Models/TabModel.cs @@ -61,12 +61,12 @@ public abstract class TabModel public abstract MyCommon.TabUsageType TabType { get; } - public virtual ConcurrentDictionary Posts + public virtual ConcurrentDictionary Posts => TabInformations.GetInstance().Posts; public int AllCount => this.ids.Count; - public long[] StatusIds => this.ids.ToArray(); + public PostId[] StatusIds => this.ids.ToArray(); public bool IsDefaultTabType => this.TabType.IsDefault(); @@ -79,10 +79,10 @@ public virtual ConcurrentDictionary Posts ///
public virtual bool IsPermanentTabType => true; - public long[] SelectedStatusIds + public PostId[] SelectedStatusIds => this.selectedStatusIds.ToArray(); - public long? SelectedStatusId + public PostId? SelectedStatusId => this.selectedStatusIds.Count > 0 ? this.selectedStatusIds[0] : null; public PostClass[] SelectedPosts @@ -96,11 +96,11 @@ public int SelectedIndex get { var statusId = this.SelectedStatusId; - return statusId != null ? this.IndexOf(statusId.Value) : -1; + return statusId != null ? this.IndexOf(statusId) : -1; } } - public long? AnchorStatusId { get; set; } + public PostId? AnchorStatusId { get; set; } public PostClass? AnchorPost { @@ -109,7 +109,7 @@ public PostClass? AnchorPost if (this.AnchorStatusId == null) return null; - if (!this.Posts.TryGetValue(this.AnchorStatusId.Value, out var post)) + if (!this.Posts.TryGetValue(this.AnchorStatusId, out var post)) return null; return post; @@ -117,11 +117,11 @@ public PostClass? AnchorPost set => this.AnchorStatusId = value?.StatusId; } - private IndexedSortedSet ids = new(); + private IndexedSortedSet ids = new(); private ConcurrentQueue addQueue = new(); - private readonly ConcurrentQueue removeQueue = new(); - private SortedSet unreadIds = new(); - private List selectedStatusIds = new(); + private readonly ConcurrentQueue removeQueue = new(); + private SortedSet unreadIds = new(); + private List selectedStatusIds = new(); private readonly object lockObj = new(); @@ -131,7 +131,7 @@ protected TabModel(string tabName) public abstract Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress progress); private readonly record struct TemporaryId( - long StatusId, + PostId StatusId, bool Read ); @@ -144,7 +144,7 @@ public virtual void AddPostQueue(PostClass post) } // 無条件に追加 - internal bool AddPostImmediately(long statusId, bool read) + internal bool AddPostImmediately(PostId statusId, bool read) { if (!this.ids.Add(statusId)) return false; @@ -155,9 +155,9 @@ internal bool AddPostImmediately(long statusId, bool read) return true; } - public IReadOnlyList AddSubmit() + public IReadOnlyList AddSubmit() { - var addedIds = new List(); + var addedIds = new List(); while (this.addQueue.TryDequeue(out var tId)) { @@ -168,10 +168,10 @@ public IReadOnlyList AddSubmit() return addedIds; } - public virtual void EnqueueRemovePost(long statusId, bool setIsDeleted) + public virtual void EnqueueRemovePost(PostId statusId, bool setIsDeleted) => this.removeQueue.Enqueue(statusId); - public virtual bool RemovePostImmediately(long statusId) + public virtual bool RemovePostImmediately(PostId statusId) { if (!this.ids.Remove(statusId)) return false; @@ -181,9 +181,9 @@ public virtual bool RemovePostImmediately(long statusId) return true; } - public IReadOnlyList RemoveSubmit() + public IReadOnlyList RemoveSubmit() { - var removedIds = new List(); + var removedIds = new List(); while (this.removeQueue.TryDequeue(out var statusId)) { @@ -269,7 +269,7 @@ private void ApplySortMode() (x, y) => Comparer.Default.Compare(x?.TextFromApi, y?.TextFromApi), }; - Comparison comparison = (x, y) => + Comparison comparison = (x, y) => { this.Posts.TryGetValue(x, out var xPost); this.Posts.TryGetValue(y, out var yPost); @@ -282,17 +282,17 @@ private void ApplySortMode() return sign * x.CompareTo(y); }; - var comparer = Comparer.Create(comparison); + var comparer = Comparer.Create(comparison); - this.ids = new IndexedSortedSet(this.ids, comparer); - this.unreadIds = new SortedSet(this.unreadIds, comparer); + this.ids = new IndexedSortedSet(this.ids, comparer); + this.unreadIds = new SortedSet(this.unreadIds, comparer); } /// /// 次に表示する未読ツイートのIDを返します。 /// ただし、未読がない場合または UnreadManage が false の場合は null を返します /// - public long? NextUnreadId + public PostId? NextUnreadId { get { @@ -317,7 +317,7 @@ public int NextUnreadIndex get { var unreadId = this.NextUnreadId; - return unreadId != null ? this.IndexOf(unreadId.Value) : -1; + return unreadId != null ? this.IndexOf(unreadId) : -1; } } @@ -339,7 +339,7 @@ public int UnreadCount /// /// 未読ツイートの ID を配列で返します /// - public long[] GetUnreadIds() + public PostId[] GetUnreadIds() { lock (this.lockObj) return this.unreadIds.ToArray(); @@ -354,7 +354,7 @@ public long[] GetUnreadIds() /// 変更するツイートのID /// 既読状態 /// 既読状態に変化があれば true、変化がなければ false - internal virtual bool SetReadState(long statusId, bool read) + internal virtual bool SetReadState(PostId statusId, bool read) { if (!this.ids.Contains(statusId)) throw new ArgumentOutOfRangeException(nameof(statusId)); @@ -365,7 +365,7 @@ internal virtual bool SetReadState(long statusId, bool read) return this.unreadIds.Add(statusId); } - public bool Contains(long statusId) + public bool Contains(PostId statusId) => this.ids.Contains(statusId); public PostClass this[int index] @@ -404,13 +404,13 @@ public PostClass this[int index] } } - public long[] GetStatusIdAt(IEnumerable indexes) + public PostId[] GetStatusIdAt(IEnumerable indexes) => indexes.Select(x => this.GetStatusIdAt(x)).ToArray(); - public long GetStatusIdAt(int index) + public PostId GetStatusIdAt(int index) => this.ids[index]; - public int[] IndexOf(long[] statusIds) + public int[] IndexOf(PostId[] statusIds) { if (statusIds == null) throw new ArgumentNullException(nameof(statusIds)); @@ -418,7 +418,7 @@ public int[] IndexOf(long[] statusIds) return statusIds.Select(x => this.IndexOf(x)).ToArray(); } - public int IndexOf(long statusId) + public int IndexOf(PostId statusId) => this.ids.IndexOf(statusId); public IEnumerable SearchPostsAll(Func stringComparer) diff --git a/OpenTween/Models/TwitterDirectMessageId.cs b/OpenTween/Models/TwitterDirectMessageId.cs new file mode 100644 index 000000000..2237ee4a8 --- /dev/null +++ b/OpenTween/Models/TwitterDirectMessageId.cs @@ -0,0 +1,41 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTween.Models +{ + public class TwitterDirectMessageId : PostId + { + public override string IdType => "twitter_dm"; + + public override string Id { get; } + + public TwitterDirectMessageId(string id) + => this.Id = id; + } +} diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index b523de6cf..34ca67fb8 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -80,7 +80,7 @@ public PostClass CreateFromStatus( { // 幻覚fav対策 (8a5717dd のコミット参照) var favTab = this.tabinfo.FavoriteTab; - isFav = favTab.Contains(originalStatus.Id); + isFav = favTab.Contains(new TwitterStatusId(originalStatus.IdStr)); } var geo = (PostClass.StatusGeo?)null; @@ -108,7 +108,9 @@ public PostClass CreateFromStatus( var quoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink) .Where(x => x != status.Id && x != originalStatus.Id) - .Distinct().ToArray(); + .Distinct() + .Select(x => new TwitterStatusId(x)) + .ToArray(); var expandedUrls = entities.OfType() .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) @@ -141,7 +143,7 @@ public PostClass CreateFromStatus( return new() { // status から生成 - StatusId = status.Id, + StatusId = new TwitterStatusId(status.IdStr), CreatedAtForSorting = createdAtForSorting, IsMe = statusUser.Id == selfUserId, @@ -159,7 +161,7 @@ public PostClass CreateFromStatus( SourceUri = sourceUri, IsFav = isFav, IsReply = retweetedStatus != null && replyToList.Any(x => x.UserId == selfUserId), - InReplyToStatusId = originalStatus.InReplyToStatusId, + InReplyToStatusId = originalStatus.InReplyToStatusIdStr != null ? new TwitterStatusId(originalStatus.InReplyToStatusIdStr) : null, InReplyToUser = originalStatus.InReplyToScreenName, InReplyToUserId = originalStatus.InReplyToUserId, @@ -172,7 +174,7 @@ public PostClass CreateFromStatus( IsOwl = isOwl, // retweetedStatus から生成 - RetweetedId = retweetedStatus?.Id, + RetweetedId = retweetedStatus != null ? new TwitterStatusId(retweetedStatus.IdStr) : null, // retweeterUser から生成 RetweetedBy = retweeterUser != null ? string.Intern(retweeterUser.ScreenName) : null, @@ -213,7 +215,9 @@ long selfUserId var (replyToList, media) = this.ExtractEntities(entities); var quoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink: null) - .Distinct().ToArray(); + .Distinct() + .Select(x => new TwitterStatusId(x)) + .ToArray(); var expandedUrls = entities.OfType() .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl)) @@ -257,7 +261,7 @@ long selfUserId return new() { - StatusId = long.Parse(eventItem.Id), + StatusId = new TwitterDirectMessageId(eventItem.Id), IsDm = true, CreatedAt = createdAt, Text = text, diff --git a/OpenTween/Models/TwitterStatusId.cs b/OpenTween/Models/TwitterStatusId.cs new file mode 100644 index 000000000..baa279588 --- /dev/null +++ b/OpenTween/Models/TwitterStatusId.cs @@ -0,0 +1,50 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) +// All rights reserved. +// +// This file is part of OpenTween. +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see , or write to +// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, +// Boston, MA 02110-1301, USA. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTween.Models +{ + public class TwitterStatusId : PostId + { + public override string IdType => "twitter_status"; + + public override string Id { get; } + + public TwitterStatusId(string id) + => this.Id = id; + + public TwitterStatusId(long id) + => this.Id = id.ToString(); + } + + public static class TwitterStatusIdExtension + { + public static TwitterStatusId ToTwitterStatusId(this PostId postId) + => postId is TwitterStatusId statusId ? statusId : throw new InvalidOperationException("Cannot convert to twitter status_id."); + } +} diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index caf7b5a3e..5e51960fe 100644 --- a/OpenTween/MyCommon.cs +++ b/OpenTween/MyCommon.cs @@ -845,14 +845,12 @@ public static string GetReadableVersion(Version version) public static string GetStatusUrl(PostClass post) { - if (post.RetweetedId == null) - return GetStatusUrl(post.ScreenName, post.StatusId); - else - return GetStatusUrl(post.ScreenName, post.RetweetedId.Value); + var statusId = post.RetweetedId ?? post.StatusId; + return GetStatusUrl(post.ScreenName, statusId.ToTwitterStatusId()); } - public static string GetStatusUrl(string screenName, long statusId) - => TwitterUrl + screenName + "/status/" + statusId; + public static string GetStatusUrl(string screenName, TwitterStatusId statusId) + => TwitterUrl + screenName + "/status/" + statusId.Id; /// /// 指定された IDictionary を元にクエリ文字列を生成します diff --git a/OpenTween/PostStatusParams.cs b/OpenTween/PostStatusParams.cs index 971e226d9..d7af5bc9c 100644 --- a/OpenTween/PostStatusParams.cs +++ b/OpenTween/PostStatusParams.cs @@ -27,6 +27,7 @@ using System.Text; using System.Threading.Tasks; using OpenTween.Connection; +using OpenTween.Models; namespace OpenTween { @@ -34,7 +35,7 @@ public class PostStatusParams { public string Text { get; set; } = ""; - public long? InReplyToStatusId { get; set; } + public PostId? InReplyToStatusId { get; set; } public IReadOnlyList MediaIds { get; set; } = Array.Empty(); diff --git a/OpenTween/TimelineListViewState.cs b/OpenTween/TimelineListViewState.cs index 2a746f1e4..9878d5b66 100644 --- a/OpenTween/TimelineListViewState.cs +++ b/OpenTween/TimelineListViewState.cs @@ -46,7 +46,7 @@ public class TimelineListViewState internal readonly record struct ListViewScroll( ScrollLockMode ScrollLockMode, - long? TopItemStatusId + PostId? TopItemStatusId ); internal enum ScrollLockMode @@ -65,9 +65,9 @@ internal enum ScrollLockMode } internal readonly record struct ListViewSelection( - long[] SelectedStatusIds, - long? SelectionMarkStatusId, - long? FocusedStatusId + PostId[] SelectedStatusIds, + PostId? SelectionMarkStatusId, + PostId? FocusedStatusId ); public TimelineListViewState(DetailsListView listView, TabModel tab) @@ -100,7 +100,7 @@ public void RestoreSelection() private ListViewScroll SaveListViewScroll(bool lockScroll) { var lockMode = this.GetScrollLockMode(lockScroll); - long? topItemStatusId = null; + PostId? topItemStatusId = null; if (lockMode == ScrollLockMode.FixedToItem || lockMode == ScrollLockMode.None) { @@ -165,7 +165,7 @@ private ListViewSelection SaveListViewSelection() { return new ListViewSelection { - SelectedStatusIds = Array.Empty(), + SelectedStatusIds = Array.Empty(), SelectionMarkStatusId = null, FocusedStatusId = null, }; @@ -179,18 +179,18 @@ private ListViewSelection SaveListViewSelection() }; } - private long? GetFocusedStatusId() + private PostId? GetFocusedStatusId() { var index = this.listView.FocusedItem?.Index ?? -1; - return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : (long?)null; + return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : null; } - private long? GetSelectionMarkStatusId() + private PostId? GetSelectionMarkStatusId() { var index = this.listView.SelectionMark; - return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : (long?)null; + return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : null; } /// @@ -214,7 +214,7 @@ private void RestoreListViewScroll(ListViewScroll listScroll, bool forceScroll) this.listView.EnsureVisible(this.listView.VirtualListSize - 1); break; case ScrollLockMode.FixedToItem: - var topIndex = listScroll.TopItemStatusId != null ? this.tab.IndexOf(listScroll.TopItemStatusId.Value) : -1; + var topIndex = listScroll.TopItemStatusId != null ? this.tab.IndexOf(listScroll.TopItemStatusId) : -1; if (topIndex != -1) { var topItem = this.listView.Items[topIndex]; @@ -255,14 +255,14 @@ private void RestoreListViewSelection(ListViewSelection listSelection) if (listSelection.FocusedStatusId != null) { - var focusedIndex = this.tab.IndexOf(listSelection.FocusedStatusId.Value); + var focusedIndex = this.tab.IndexOf(listSelection.FocusedStatusId); if (focusedIndex != -1) this.listView.Items[focusedIndex].Focused = true; } if (listSelection.SelectionMarkStatusId != null) { - var selectionMarkIndex = this.tab.IndexOf(listSelection.SelectionMarkStatusId.Value); + var selectionMarkIndex = this.tab.IndexOf(listSelection.SelectionMarkStatusId); if (selectionMarkIndex != -1) this.listView.SelectionMark = selectionMarkIndex; } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 9643baed4..9dd7d350c 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -167,7 +167,7 @@ public partial class TweenMain : OTBaseForm // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない) /// リプライ先のステータスID・スクリーン名 - private (long StatusId, string ScreenName)? inReplyTo = null; + private (PostId StatusId, string ScreenName)? inReplyTo = null; // 時速表示用 private readonly List postTimestamps = new(); @@ -219,8 +219,8 @@ string After private List? urlUndoBuffer = null; private readonly record struct ReplyChain( - long OriginalId, - long InReplyToId, + PostId OriginalId, + PostId InReplyToId, TabModel OriginalTab ); @@ -258,7 +258,7 @@ internal enum SEARCHTYPE private readonly record struct StatusTextHistory( string Status, - (long StatusId, string ScreenName)? InReplyTo = null + (PostId StatusId, string ScreenName)? InReplyTo = null ); private readonly HookGlobalHotkey hookGlobalHotkey; @@ -1048,7 +1048,7 @@ private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCo if (MyCommon.IsNullOrEmpty(bText)) return; var image = this.iconCache.TryGetFromCache(post.ImageUrl); - this.gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl); + this.gh.Notify(nt, post.StatusId.Id, title.ToString(), bText, image?.Image, post.ImageUrl); } } else @@ -1418,7 +1418,7 @@ private async Task RefreshTabAsync(TabModel tab, bool backward) } } - private async Task FavAddAsync(long statusId, TabModel tab) + private async Task FavAddAsync(PostId statusId, TabModel tab) { await this.workerSemaphore.WaitAsync(); @@ -1440,7 +1440,7 @@ private async Task FavAddAsync(long statusId, TabModel tab) } } - private async Task FavAddAsyncInternal(IProgress p, CancellationToken ct, long statusId, TabModel tab) + private async Task FavAddAsyncInternal(IProgress p, CancellationToken ct, PostId statusId, TabModel tab) { if (ct.IsCancellationRequested) return; @@ -1460,9 +1460,10 @@ await Task.Run(async () => try { + var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId(); try { - await this.tw.Api.FavoritesCreate(post.RetweetedId ?? post.StatusId) + await this.tw.Api.FavoritesCreate(twitterStatusId) .IgnoreResponse() .ConfigureAwait(false); } @@ -1474,7 +1475,7 @@ await this.tw.Api.FavoritesCreate(post.RetweetedId ?? post.StatusId) if (this.settings.Common.RestrictFavCheck) { - var status = await this.tw.Api.StatusesShow(post.RetweetedId ?? post.StatusId) + var status = await this.tw.Api.StatusesShow(twitterStatusId) .ConfigureAwait(false); if (status.Favorited != true) @@ -1538,7 +1539,7 @@ await this.tw.Api.FavoritesCreate(post.RetweetedId ?? post.StatusId) } } - private async Task FavRemoveAsync(IReadOnlyList statusIds, TabModel tab) + private async Task FavRemoveAsync(IReadOnlyList statusIds, TabModel tab) { await this.workerSemaphore.WaitAsync(); @@ -1560,7 +1561,7 @@ private async Task FavRemoveAsync(IReadOnlyList statusIds, TabModel tab) } } - private async Task FavRemoveAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList statusIds, TabModel tab) + private async Task FavRemoveAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList statusIds, TabModel tab) { if (ct.IsCancellationRequested) return; @@ -1568,7 +1569,7 @@ private async Task FavRemoveAsyncInternal(IProgress p, CancellationToken if (!CheckAccountValid()) throw new WebApiException("Auth error. Check your account"); - var successIds = new List(); + var successIds = new List(); await Task.Run(async () => { @@ -1586,9 +1587,11 @@ await Task.Run(async () => if (!post.IsFav) continue; + var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId(); + try { - await this.tw.Api.FavoritesDestroy(post.RetweetedId ?? post.StatusId) + await this.tw.Api.FavoritesDestroy(twitterStatusId) .IgnoreResponse() .ConfigureAwait(false); } @@ -1796,7 +1799,7 @@ await Task.Run(async () => await this.RefreshTabAsync(); } - private async Task RetweetAsync(IReadOnlyList statusIds) + private async Task RetweetAsync(IReadOnlyList statusIds) { await this.workerSemaphore.WaitAsync(); @@ -1818,7 +1821,7 @@ private async Task RetweetAsync(IReadOnlyList statusIds) } } - private async Task RetweetAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList statusIds) + private async Task RetweetAsyncInternal(IProgress p, CancellationToken ct, IReadOnlyList statusIds) { if (ct.IsCancellationRequested) return; @@ -2389,9 +2392,9 @@ private async Task DoStatusDelete() try { - if (post.IsDm) + if (post.StatusId is TwitterDirectMessageId dmId) { - await this.tw.Api.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture)); + await this.tw.Api.DirectMessagesEventsDestroy(dmId); } else { @@ -2399,7 +2402,7 @@ private async Task DoStatusDelete() { // 自分が RT したツイート (自分が RT した自分のツイートも含む) // => RT を取り消し - await this.tw.Api.StatusesDestroy(post.StatusId) + await this.tw.Api.StatusesDestroy(post.StatusId.ToTwitterStatusId()) .IgnoreResponse(); } else @@ -2410,14 +2413,14 @@ await this.tw.Api.StatusesDestroy(post.StatusId) { // 他人に RT された自分のツイート // => RT 元の自分のツイートを削除 - await this.tw.Api.StatusesDestroy(post.RetweetedId.Value) + await this.tw.Api.StatusesDestroy(post.RetweetedId.ToTwitterStatusId()) .IgnoreResponse(); } else { // 自分のツイート // => ツイートを削除 - await this.tw.Api.StatusesDestroy(post.StatusId) + await this.tw.Api.StatusesDestroy(post.StatusId.ToTwitterStatusId()) .IgnoreResponse(); } } @@ -5061,7 +5064,7 @@ private void GoSamePostToAnotherTab(bool left) if (targetTab.TabType == MyCommon.TabUsageType.DirectMessage) continue; - var foundIndex = targetTab.IndexOf(selectedStatusId.Value); + var foundIndex = targetTab.IndexOf(selectedStatusId); if (foundIndex != -1) { this.ListTab.SelectedIndex = tabidx; @@ -5197,7 +5200,7 @@ private void GoAnchor() if (anchorStatusId == null) return; - var idx = this.CurrentTab.IndexOf(anchorStatusId.Value); + var idx = this.CurrentTab.IndexOf(anchorStatusId); if (idx == -1) return; @@ -5314,7 +5317,7 @@ private async Task GoInReplyToPostTree() { try { - var post = await this.tw.GetStatusApi(false, currentPost.StatusId); + var post = await this.tw.GetStatusApi(false, currentPost.StatusId.ToTwitterStatusId()); currentPost = currentPost with { @@ -5340,11 +5343,11 @@ private async Task GoInReplyToPostTree() { this.replyChains = new Stack(); } - this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass)); + this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId, curTabClass)); int inReplyToIndex; string inReplyToTabName; - var inReplyToId = currentPost.InReplyToStatusId.Value; + var inReplyToId = currentPost.InReplyToStatusId; var inReplyToUser = currentPost.InReplyToUser; var inReplyToPosts = from tab in this.statuses.Tabs @@ -5362,7 +5365,7 @@ from post in tab.Posts.Values { await Task.Run(async () => { - var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value) + var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.ToTwitterStatusId()) .ConfigureAwait(false); post.IsRead = true; @@ -5373,7 +5376,7 @@ await Task.Run(async () => catch (WebApiException ex) { this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)"; - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId())); return; } @@ -5382,7 +5385,7 @@ await Task.Run(async () => inReplyPost = inReplyToPosts.FirstOrDefault(); if (inReplyPost == null) { - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId())); return; } } @@ -5610,10 +5613,8 @@ private void TrimPostChain() } } - private bool GoStatus(long statusId) + private bool GoStatus(PostId statusId) { - if (statusId == 0) return false; - var tab = this.statuses.Tabs .Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage) .Where(x => x.Contains(statusId)) @@ -5634,10 +5635,8 @@ private bool GoStatus(long statusId) return true; } - private bool GoDirectMessage(long statusId) + private bool GoDirectMessage(PostId statusId) { - if (statusId == 0) return false; - var tab = this.statuses.DirectMessageTab; var index = tab.IndexOf(statusId); @@ -5844,7 +5843,7 @@ static void ShowFormatErrorDialog(IWin32Window owner) try { - var statusId = long.Parse(match.Groups["StatusId"].Value); + var statusId = new TwitterStatusId(match.Groups["StatusId"].Value); await this.OpenRelatedTab(statusId); } catch (OverflowException) @@ -5892,7 +5891,7 @@ private void SaveLogMenuItem_Click(object sender, EventArgs e) "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" + post.CreatedAt.ToLocalTimeString() + "\t" + post.ScreenName + "\t" + - post.StatusId + "\t" + + post.StatusId.Id + "\t" + post.ImageUrl + "\t" + "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" + protect); @@ -5909,7 +5908,7 @@ private void SaveLogMenuItem_Click(object sender, EventArgs e) "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" + post.CreatedAt.ToLocalTimeString() + "\t" + post.ScreenName + "\t" + - post.StatusId + "\t" + + post.StatusId.Id + "\t" + post.ImageUrl + "\t" + "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" + protect); @@ -7323,10 +7322,10 @@ private async Task DoRepliedStatusOpen() { if (MyCommon.IsKeyDown(Keys.Shift)) { - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId())); return; } - if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost)) + if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId, out var repPost)) { MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi); } @@ -7334,12 +7333,12 @@ private async Task DoRepliedStatusOpen() { foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch)) { - if (tb == null || !tb.Contains(currentPost.InReplyToStatusId.Value)) break; - repPost = tb.Posts[currentPost.InReplyToStatusId.Value]; + if (tb == null || !tb.Contains(currentPost.InReplyToStatusId)) break; + repPost = tb.Posts[currentPost.InReplyToStatusId]; MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi); return; } - await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value)); + await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId())); } } } @@ -7901,7 +7900,7 @@ private async Task OpenInternalUriAsync(Uri uri) var match = Regex.Match(uri.AbsolutePath, @"^/status/(\d+)$"); if (match.Success) { - var statusId = long.Parse(match.Groups[1].Value); + var statusId = new TwitterStatusId(match.Groups[1].Value); await this.OpenRelatedTab(statusId); return; } @@ -9082,7 +9081,7 @@ private async void RtCountMenuItem_Click(object sender, EventArgs e) try { - var task = this.tw.Api.StatusesShow(statusId); + var task = this.tw.Api.StatusesShow(statusId.ToTwitterStatusId()); status = await dialog.WaitForAsync(this, task); } catch (WebApiException ex) @@ -9273,14 +9272,14 @@ private async void ShowRelatedStatusesMenuItem_Click(object sender, EventArgs e) /// /// 表示するツイートのID /// 名前の重複が多すぎてタブを作成できない場合 - public async Task OpenRelatedTab(long statusId) + public async Task OpenRelatedTab(PostId statusId) { var post = this.statuses[statusId]; if (post == null) { try { - post = await this.tw.GetStatusApi(false, statusId); + post = await this.tw.GetStatusApi(false, statusId.ToTwitterStatusId()); } catch (WebApiException ex) { @@ -9456,7 +9455,7 @@ private async Task OpenUserAppointUrl() xUrl = xUrl.Replace("{ID}", post.ScreenName); var statusId = post.RetweetedId ?? post.StatusId; - xUrl = xUrl.Replace("{STATUS}", statusId.ToString()); + xUrl = xUrl.Replace("{STATUS}", statusId.Id); await MyCommon.OpenInBrowserAsync(this, xUrl); } @@ -9483,11 +9482,11 @@ await this.InvokeAsync(() => this.BringToFront(); if (e.NotifyType == GrowlHelper.NotifyType.DirectMessage) { - if (!this.GoDirectMessage(e.StatusId)) this.StatusText.Focus(); + if (!this.GoDirectMessage(new TwitterStatusId(e.StatusId))) this.StatusText.Focus(); } else { - if (!this.GoStatus(e.StatusId)) this.StatusText.Focus(); + if (!this.GoStatus(new TwitterStatusId(e.StatusId))) this.StatusText.Focus(); } }); } diff --git a/OpenTween/TweetDetailsView.cs b/OpenTween/TweetDetailsView.cs index 4a683851f..95d1ca65f 100644 --- a/OpenTween/TweetDetailsView.cs +++ b/OpenTween/TweetDetailsView.cs @@ -326,7 +326,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) var loadingReplyHtml = string.Empty; if (post.InReplyToStatusId != null) - loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true); + loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId, Properties.Resources.LoadingText, isReply: true); var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml; @@ -337,7 +337,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList(); if (post.InReplyToStatusId != null) - loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true)); + loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId, isReply: true)); var quoteHtmls = await Task.WhenAll(loadTweetTasks); @@ -351,14 +351,14 @@ private async Task AppendQuoteTweetAsync(PostClass post) this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body); } - private async Task CreateQuoteTweetHtml(long statusId, bool isReply) + private async Task CreateQuoteTweetHtml(PostId statusId, bool isReply) { var post = TabInformations.GetInstance()[statusId]; if (post == null) { try { - post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId) + post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId.ToTwitterStatusId()) .ConfigureAwait(false); } catch (WebApiException ex) @@ -384,14 +384,14 @@ internal static string FormatQuoteTweetHtml(PostClass post, bool isReply) return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply); } - internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply) + internal static string FormatQuoteTweetHtml(PostId statusId, string innerHtml, bool isReply) { var blockClassName = "quote-tweet"; if (isReply) blockClassName += " reply"; - return "" + + return "" + $"
{innerHtml}
" + "
"; } diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index 78ebf84f7..3d43e8904 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -255,7 +255,7 @@ await this.SendDirectMessage(param.Text, mediaId) var response = await this.Api.StatusesUpdate( param.Text, - param.InReplyToStatusId, + param.InReplyToStatusId?.ToTwitterStatusId(), param.MediaIds, param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, @@ -359,7 +359,7 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) .ConfigureAwait(false); } - public async Task PostRetweet(long id, bool read) + public async Task PostRetweet(PostId id, bool read) { this.CheckAccountState(); @@ -370,7 +370,7 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) var target = post.RetweetedId ?? id; // 再RTの場合は元発言をRT - var response = await this.Api.StatusesRetweet(target) + var response = await this.Api.StatusesRetweet(target.ToTwitterStatusId()) .ConfigureAwait(false); var status = await response.LoadJsonAsync() @@ -379,7 +379,8 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) // 二重取得回避 lock (this.lockObj) { - if (TabInformations.GetInstance().ContainsKey(status.Id)) + var statusId = new TwitterStatusId(status.IdStr); + if (TabInformations.GetInstance().ContainsKey(statusId)) return null; } @@ -611,7 +612,7 @@ public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTab tab.OldestId = minimumId.Value; } - public async Task GetStatusApi(bool read, long id) + public async Task GetStatusApi(bool read, TwitterStatusId id) { this.CheckAccountState(); @@ -626,7 +627,7 @@ public async Task GetStatusApi(bool read, long id) return item; } - public async Task GetStatusApi(bool read, long id, TabModel tab) + public async Task GetStatusApi(bool read, TwitterStatusId id, TabModel tab) { var post = await this.GetStatusApi(read, id) .ConfigureAwait(false); @@ -656,13 +657,14 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) // 二重取得回避 lock (this.lockObj) { + var id = new TwitterStatusId(status.IdStr); if (tab == null) { - if (TabInformations.GetInstance().ContainsKey(status.Id)) continue; + if (TabInformations.GetInstance().ContainsKey(id)) continue; } else { - if (tab.Contains(status.Id)) continue; + if (tab.Contains(id)) continue; } } @@ -697,7 +699,8 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) // 二重取得回避 lock (this.lockObj) { - if (tab.Contains(status.Id)) continue; + if (tab.Contains(new TwitterStatusId(status.IdStr))) + continue; } var post = this.CreatePostsFromStatusData(status); @@ -724,7 +727,8 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) // 二重取得回避 lock (this.lockObj) { - if (favTab.Contains(status.Id)) continue; + if (favTab.Contains(new TwitterStatusId(status.IdStr))) + continue; } var post = this.CreatePostsFromStatusData(status, true); @@ -763,17 +767,17 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。 ///
/// posts の中から検索されたリプライチェインの末端 - internal static PostClass FindTopOfReplyChain(IDictionary posts, long startStatusId) + internal static PostClass FindTopOfReplyChain(IDictionary posts, PostId startStatusId) { if (!posts.ContainsKey(startStatusId)) - throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId)); + throw new ArgumentException("startStatusId (" + startStatusId.Id + ") が posts の中から見つかりませんでした。", nameof(startStatusId)); var nextPost = posts[startStatusId]; while (nextPost.InReplyToStatusId != null) { - if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value)) + if (!posts.ContainsKey(nextPost.InReplyToStatusId)) break; - nextPost = posts[nextPost.InReplyToStatusId.Value]; + nextPost = posts[nextPost.InReplyToStatusId]; } return nextPost; @@ -787,14 +791,14 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) { var originalPost = targetPost with { - StatusId = targetPost.RetweetedId.Value, + StatusId = targetPost.RetweetedId, RetweetedId = null, RetweetedBy = null, }; targetPost = originalPost; } - var relPosts = new Dictionary(); + var relPosts = new Dictionary(); if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null) { // 検索結果対応 @@ -805,7 +809,7 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) } else { - p = await this.GetStatusApi(read, targetPost.StatusId) + p = await this.GetStatusApi(read, targetPost.StatusId.ToTwitterStatusId()) .ConfigureAwait(false); targetPost = p; } @@ -819,14 +823,14 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) var loopCount = 1; while (nextPost.InReplyToStatusId != null && loopCount++ <= 20) { - var inReplyToId = nextPost.InReplyToStatusId.Value; + var inReplyToId = nextPost.InReplyToStatusId; var inReplyToPost = TabInformations.GetInstance()[inReplyToId]; if (inReplyToPost == null) { try { - inReplyToPost = await this.GetStatusApi(read, inReplyToId) + inReplyToPost = await this.GetStatusApi(read, inReplyToId.ToTwitterStatusId()) .ConfigureAwait(false); } catch (WebApiException ex) @@ -847,11 +851,9 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast()); foreach (var match in ma) { - if (long.TryParse(match.Groups["StatusId"].Value, out var statusId)) + var statusId = new TwitterStatusId(match.Groups["StatusId"].Value); + if (!relPosts.ContainsKey(statusId)) { - if (relPosts.ContainsKey(statusId)) - continue; - var p = TabInformations.GetInstance()[statusId]; if (p == null) { @@ -884,7 +886,7 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) continue; // リプライチェーンが繋がらないツイートは除外 - if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId.Value)) + if (post.InReplyToStatusId == null || !relPosts.ContainsKey(post.InReplyToStatusId)) continue; relPosts.Add(post.StatusId, post); @@ -913,7 +915,7 @@ public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab) private async Task GetConversationPosts(PostClass firstPost, PostClass targetPost) { var conversationId = firstPost.StatusId; - var query = $"conversation_id:{conversationId}"; + var query = $"conversation_id:{conversationId.Id}"; if (targetPost.InReplyToUser != null && targetPost.InReplyToUser != targetPost.ScreenName) query += $" (from:{targetPost.ScreenName} to:{targetPost.InReplyToUser}) OR (from:{targetPost.InReplyToUser} to:{targetPost.ScreenName})"; From da578f0411c21107450fe016e910e70ba40cc879 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 3 Jul 2023 01:15:11 +0900 Subject: [PATCH 15/21] =?UTF-8?q?C#=20=E3=81=AE=E8=A8=80=E8=AA=9E=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=83=A7=E3=83=B3=E3=82=92=20C#=2011.0=20?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/OpenTween.Tests.csproj | 2 +- OpenTween/OpenTween.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index 1b99bd52a..a143b1727 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -3,7 +3,7 @@ OpenTween net48 - 10.0 + 11.0 enable true diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj index 29b33ed36..b756c986d 100644 --- a/OpenTween/OpenTween.csproj +++ b/OpenTween/OpenTween.csproj @@ -4,7 +4,7 @@ WinExe true net48 - 10.0 + 11.0 bin\$(Configuration)\ true false From 3e1059fbba2c49279cb7f806261ade737b2b893c Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 3 Jul 2023 02:08:11 +0900 Subject: [PATCH 16/21] =?UTF-8?q?Raw=20string=20literals=E3=82=92=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Api/BitlyApiTest.cs | 4 +- .../DataModel/TwitterMessageEventListTest.cs | 22 +-- .../TwitterUser_20190520ChangesTest.cs | 94 +++++++------ OpenTween.Tests/Api/ImgurApiTest.cs | 78 ++++++----- .../Api/MicrosoftTranslatorApiTest.cs | 22 +-- OpenTween.Tests/Api/MobypictureApiTest.cs | 14 +- OpenTween.Tests/Api/TwitterApiStatusTest.cs | 2 +- OpenTween.Tests/Api/TwitterApiTest.cs | 42 +++--- .../Connection/TwitterApiConnectionTest.cs | 10 +- .../Connection/TwitterComCookieHandlerTest.cs | 2 +- OpenTween.Tests/Models/PostClassTest.cs | 10 +- .../Models/TwitterPostFactoryTest.cs | 30 ++-- OpenTween.Tests/MyCommonTest.cs | 2 +- OpenTween.Tests/ShortUrlTest.cs | 8 +- .../Services/FoursquareCheckinTest.cs | 132 ++++++++++-------- .../Thumbnail/Services/ImgAzyobuziNetTest.cs | 4 +- .../Services/MetaThumbnailServiceTest.cs | 124 ++++++++-------- .../Thumbnail/Services/TinamiTest.cs | 42 +++--- .../Thumbnail/Services/TumblrTest.cs | 72 +++++----- OpenTween.Tests/TweenMainTest.cs | 6 +- OpenTween.Tests/TweetDetailsViewTest.cs | 16 +-- OpenTween.Tests/TweetFormatterTest.cs | 34 ++--- OpenTween.Tests/TwitterTest.cs | 6 +- OpenTween/Api/MicrosoftTranslatorApi.cs | 2 +- OpenTween/Api/TwitterApi.cs | 45 +++--- OpenTween/Models/PostClass.cs | 2 +- OpenTween/Models/TwitterPostFactory.cs | 6 +- .../Services/MetaThumbnailService.cs | 4 +- OpenTween/Tween.cs | 14 +- OpenTween/TweetDetailsView.cs | 4 +- OpenTween/TweetFormatter.cs | 10 +- 31 files changed, 449 insertions(+), 414 deletions(-) diff --git a/OpenTween.Tests/Api/BitlyApiTest.cs b/OpenTween.Tests/Api/BitlyApiTest.cs index b40a3ea2b..b8c7f4a66 100644 --- a/OpenTween.Tests/Api/BitlyApiTest.cs +++ b/OpenTween.Tests/Api/BitlyApiTest.cs @@ -132,7 +132,7 @@ public async Task GetAccessTokenAsync_Test() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent("{\"access_token\": \"abcdefg\"}"), + Content = new StringContent("""{"access_token": "abcdefg"}"""), }; }); @@ -154,7 +154,7 @@ public async Task GetAccessTokenAsync_ErrorResponseTest() { return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent("{\"status_code\": \"500\", \"status_txt\": \"MISSING_ARG_USERNAME\"}"), + Content = new StringContent("""{"status_code": "500", "status_txt": "MISSING_ARG_USERNAME"}"""), }; }); diff --git a/OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs b/OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs index 872c96ed5..91569b11c 100644 --- a/OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs +++ b/OpenTween.Tests/Api/DataModel/TwitterMessageEventListTest.cs @@ -29,16 +29,18 @@ public class TwitterMessageEventListTest [Fact] public void Deserialize_AppsTest() { - var json = @"{ - ""events"": [], - ""apps"": { - ""258901"": { - ""id"": ""258901"", - ""name"": ""Twitter for Android"", - ""url"": ""http://twitter.com/download/android"" - } - } -}"; + var json = """ + { + "events": [], + "apps": { + "258901": { + "id": "258901", + "name": "Twitter for Android", + "url": "http://twitter.com/download/android" + } + } + } + """; var result = MyCommon.CreateDataFromJson(json); Assert.Single(result.Apps); diff --git a/OpenTween.Tests/Api/DataModel/TwitterUser_20190520ChangesTest.cs b/OpenTween.Tests/Api/DataModel/TwitterUser_20190520ChangesTest.cs index e4a99d9d5..cc38ff244 100644 --- a/OpenTween.Tests/Api/DataModel/TwitterUser_20190520ChangesTest.cs +++ b/OpenTween.Tests/Api/DataModel/TwitterUser_20190520ChangesTest.cs @@ -32,52 +32,54 @@ public void ParseJsonTest() // (廃止されるフィールドに null がセットされる) // https://twittercommunity.com/t/124732 - var json = @"{ - ""id"": 6253282, - ""id_str"": ""6253282"", - ""name"": ""Twitter API"", - ""screen_name"": ""TwitterAPI"", - ""location"": ""San Francisco, CA"", - ""profile_location"": null, - ""description"": ""The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website."", - ""url"": ""https:\/\/t.co\/8IkCzCDr19"", - ""protected"": false, - ""followers_count"": 6133601, - ""friends_count"": 12, - ""listed_count"": 12935, - ""created_at"": ""Wed May 23 06:01:13 +0000 2007"", - ""favourites_count"": 31, - ""utc_offset"": null, - ""time_zone"": null, - ""geo_enabled"": null, - ""verified"": true, - ""statuses_count"": 3657, - ""lang"": null, - ""contributors_enabled"": null, - ""is_translator"": null, - ""is_translation_enabled"": null, - ""profile_background_color"": null, - ""profile_background_image_url"": null, - ""profile_background_image_url_https"": null, - ""profile_background_tile"": null, - ""profile_image_url"": null, - ""profile_image_url_https"": ""https:\/\/pbs.twimg.com\/profile_images\/942858479592554497\/BbazLO9L_normal.jpg"", - ""profile_banner_url"": ""https:\/\/pbs.twimg.com\/profile_banners\/6253282\/1497491515"", - ""profile_image_extensions_alt_text"": null, - ""profile_banner_extensions_alt_text"": null, - ""profile_link_color"": null, - ""profile_sidebar_border_color"": null, - ""profile_sidebar_fill_color"": null, - ""profile_text_color"": null, - ""profile_use_background_image"": null, - ""has_extended_profile"": null, - ""default_profile"": false, - ""default_profile_image"": false, - ""following"": null, - ""follow_request_sent"": null, - ""notifications"": null, - ""translator_type"": null -}"; + var json = """ + { + "id": 6253282, + "id_str": "6253282", + "name": "Twitter API", + "screen_name": "TwitterAPI", + "location": "San Francisco, CA", + "profile_location": null, + "description": "The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website.", + "url": "https:\/\/t.co\/8IkCzCDr19", + "protected": false, + "followers_count": 6133601, + "friends_count": 12, + "listed_count": 12935, + "created_at": "Wed May 23 06:01:13 +0000 2007", + "favourites_count": 31, + "utc_offset": null, + "time_zone": null, + "geo_enabled": null, + "verified": true, + "statuses_count": 3657, + "lang": null, + "contributors_enabled": null, + "is_translator": null, + "is_translation_enabled": null, + "profile_background_color": null, + "profile_background_image_url": null, + "profile_background_image_url_https": null, + "profile_background_tile": null, + "profile_image_url": null, + "profile_image_url_https": "https:\/\/pbs.twimg.com\/profile_images\/942858479592554497\/BbazLO9L_normal.jpg", + "profile_banner_url": "https:\/\/pbs.twimg.com\/profile_banners\/6253282\/1497491515", + "profile_image_extensions_alt_text": null, + "profile_banner_extensions_alt_text": null, + "profile_link_color": null, + "profile_sidebar_border_color": null, + "profile_sidebar_fill_color": null, + "profile_text_color": null, + "profile_use_background_image": null, + "has_extended_profile": null, + "default_profile": false, + "default_profile_image": false, + "following": null, + "follow_request_sent": null, + "notifications": null, + "translator_type": null + } + """; TwitterUser.ParseJson(json); } } diff --git a/OpenTween.Tests/Api/ImgurApiTest.cs b/OpenTween.Tests/Api/ImgurApiTest.cs index de9a13995..0114f3cb4 100644 --- a/OpenTween.Tests/Api/ImgurApiTest.cs +++ b/OpenTween.Tests/Api/ImgurApiTest.cs @@ -48,37 +48,39 @@ public async Task UploadFileAsync_Test() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(@" - - aaaaaaa - てすと - - 1234567890 - image/png - false - 300 - 300 - 1000 - 0 - 0 - - false - -
- - 0 - false - false - false - - 0 - - 0 - false - aaaaaaaaaaaaaaa - - https://i.imgur.com/aaaaaaa.png -"), + Content = new StringContent(""" + + + aaaaaaa + てすと + + 1234567890 + image/png + false + 300 + 300 + 1000 + 0 + 0 + + false + +
+ + 0 + false + false + false + + 0 + + 0 + false + aaaaaaaaaaaaaaa + + https://i.imgur.com/aaaaaaa.png + + """), }; }); @@ -104,12 +106,14 @@ public async Task UploadFileAsync_ErrorResponseTest() return new HttpResponseMessage(HttpStatusCode.BadRequest) { - Content = new StringContent(@" - - No image data was sent to the upload api - /3/image.xml - POST -"), + Content = new StringContent(""" + + + No image data was sent to the upload api + /3/image.xml + POST + + """), }; }); diff --git a/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs b/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs index 2812623a3..4587f02f5 100644 --- a/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs +++ b/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs @@ -73,16 +73,18 @@ public async Task TranslateAsync_Test() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(@"[ - { - ""translations"": [ - { - ""text"": ""ほげほげ"", - ""to"": ""ja"" - } - ] - } -]"), + Content = new StringContent(""" + [ + { + "translations": [ + { + "text": "ほげほげ", + "to": "ja" + } + ] + } + ] + """), }; }); diff --git a/OpenTween.Tests/Api/MobypictureApiTest.cs b/OpenTween.Tests/Api/MobypictureApiTest.cs index 42f45a490..dc0899dc7 100644 --- a/OpenTween.Tests/Api/MobypictureApiTest.cs +++ b/OpenTween.Tests/Api/MobypictureApiTest.cs @@ -45,12 +45,14 @@ public async Task UploadFileAsync_Test() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(@" - - - https://www.mobypicture.com/user/OpenTween/view/00000000 - -"), + Content = new StringContent(""" + + + + https://www.mobypicture.com/user/OpenTween/view/00000000 + + + """), }; }); diff --git a/OpenTween.Tests/Api/TwitterApiStatusTest.cs b/OpenTween.Tests/Api/TwitterApiStatusTest.cs index 187c01568..fcd982715 100644 --- a/OpenTween.Tests/Api/TwitterApiStatusTest.cs +++ b/OpenTween.Tests/Api/TwitterApiStatusTest.cs @@ -244,7 +244,7 @@ public void UpdateFromJsonTest() { var status = new TwitterApiStatus(); - var json = "{\"resources\":{\"statuses\":{\"/statuses/home_timeline\":{\"limit\":150,\"remaining\":100,\"reset\":1356998400}}}}"; + var json = """{"resources":{"statuses":{"/statuses/home_timeline":{"limit":150,"remaining":100,"reset":1356998400}}}}"""; Assert.Raises( x => status.AccessLimitUpdated += x, diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index cd48cc5ad..202d529da 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -700,25 +700,27 @@ await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg") public async Task DirectMessagesEventsNew_Test() { var mock = new Mock(); - var responseText = @"{ - ""event"": { - ""type"": ""message_create"", - ""message_create"": { - ""target"": { - ""recipient_id"": ""12345"" - }, - ""message_data"": { - ""text"": ""hogehoge"", - ""attachment"": { - ""type"": ""media"", - ""media"": { - ""id"": ""67890"" - } - } - } - } - } -}"; + var responseText = """ + { + "event": { + "type": "message_create", + "message_create": { + "target": { + "recipient_id": "12345" + }, + "message_data": { + "text": "hogehoge", + "attachment": { + "type": "media", + "media": { + "id": "67890" + } + } + } + } + } + } + """; mock.Setup(x => x.PostJsonAsync( new Uri("direct_messages/events/new.json", UriKind.Relative), @@ -1355,7 +1357,7 @@ public async Task MediaMetadataCreate_Test() mock.Setup(x => x.PostJsonAsync( new Uri("https://upload.twitter.com/1.1/media/metadata/create.json", UriKind.Absolute), - "{\"media_id\": \"12345\", \"alt_text\": {\"text\": \"hogehoge\"}}") + """{"media_id": "12345", "alt_text": {"text": "hogehoge"}}""") ) .Returns(Task.CompletedTask); diff --git a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs index 8f840e8e1..05b1fa53d 100644 --- a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs +++ b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs @@ -209,7 +209,7 @@ public async Task GetAsync_ErrorJsonTest() { return new HttpResponseMessage(HttpStatusCode.Forbidden) { - Content = new StringContent("{\"errors\":[{\"code\":187,\"message\":\"Status is a duplicate.\"}]}"), + Content = new StringContent("""{"errors":[{"code":187,"message":"Status is a duplicate."}]}"""), }; }); @@ -452,14 +452,14 @@ public async Task PostJsonAsync_Test() var body = await x.Content.ReadAsStringAsync() .ConfigureAwait(false); - Assert.Equal("{\"aaaa\": 1111}", body); + Assert.Equal("""{"aaaa": 1111}""", body); return new HttpResponseMessage(HttpStatusCode.NoContent); }); var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - await apiConnection.PostJsonAsync(endpoint, "{\"aaaa\": 1111}") + await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}""") .ConfigureAwait(false); Assert.Equal(0, mockHandler.QueueCount); @@ -484,7 +484,7 @@ public async Task PostJsonAsync_T_Test() var body = await x.Content.ReadAsStringAsync() .ConfigureAwait(false); - Assert.Equal("{\"aaaa\": 1111}", body); + Assert.Equal("""{"aaaa": 1111}""", body); return new HttpResponseMessage(HttpStatusCode.OK) { @@ -494,7 +494,7 @@ public async Task PostJsonAsync_T_Test() var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var response = await apiConnection.PostJsonAsync(endpoint, "{\"aaaa\": 1111}") + var response = await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}""") .ConfigureAwait(false); var result = await response.LoadJsonAsync() diff --git a/OpenTween.Tests/Connection/TwitterComCookieHandlerTest.cs b/OpenTween.Tests/Connection/TwitterComCookieHandlerTest.cs index 822fa5814..994baf6ba 100644 --- a/OpenTween.Tests/Connection/TwitterComCookieHandlerTest.cs +++ b/OpenTween.Tests/Connection/TwitterComCookieHandlerTest.cs @@ -37,7 +37,7 @@ public class TwitterComCookieHandlerTest [Fact] public void ParseCookie_Test() { - var cookie = "guest_id_marketing=hoge; guest_id_ads=hoge; personalization_id=\"hoge\"; guest_id=hoge; ct0=aaaaaaaaaa; kdt=hoge; twid=hoge; auth_token=bbbbbbbbbb; dnt=1"; + var cookie = """guest_id_marketing=hoge; guest_id_ads=hoge; personalization_id="hoge"; guest_id=hoge; ct0=aaaaaaaaaa; kdt=hoge; twid=hoge; auth_token=bbbbbbbbbb; dnt=1"""; var innerHandler = Mock.Of(); using var handler = new TwitterComCookieHandler(innerHandler, cookie); Assert.Equal("aaaaaaaaaa", handler.CsrfToken); diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index 943e742e7..f4f32a7c6 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -83,7 +83,7 @@ public void SourceHtml_Test() SourceUri = new Uri("http://twitter.com/"), }; - Assert.Equal("Twitter Web Client", post.SourceHtml); + Assert.Equal("""Twitter Web Client""", post.SourceHtml); } [Fact] @@ -107,7 +107,7 @@ public void SourceHtml_EscapeTest() SourceUri = new Uri("http://example.com/?aaa=123&bbb=456"), }; - Assert.Equal("<script>alert(1)</script>", post.SourceHtml); + Assert.Equal("""<script>alert(1)</script>""", post.SourceHtml); } [Fact] @@ -326,7 +326,7 @@ public async Task ExpandedUrls_BasicScenario() var post = new PostClass { - Text = "bit.ly/abcde", + Text = """bit.ly/abcde""", ExpandedUrls = new[] { new FakeExpandedUrlInfo( @@ -350,7 +350,7 @@ public async Task ExpandedUrls_BasicScenario() Assert.Equal("http://bit.ly/abcde", urlInfo.ExpandedUrl); Assert.Equal("http://bit.ly/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa")); Assert.Equal(new[] { "http://bit.ly/abcde" }, post.GetExpandedUrls()); - Assert.Equal("bit.ly/abcde", post.Text); + Assert.Equal("""bit.ly/abcde""", post.Text); // bit.ly 展開後の URL は「http://example.com/abcde」 urlInfo.FakeResult.SetResult("http://example.com/abcde"); @@ -362,7 +362,7 @@ public async Task ExpandedUrls_BasicScenario() Assert.Equal("http://example.com/abcde", urlInfo.ExpandedUrl); Assert.Equal("http://example.com/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa")); Assert.Equal(new[] { "http://example.com/abcde" }, post.GetExpandedUrls()); - Assert.Equal("bit.ly/abcde", post.Text); + Assert.Equal("""bit.ly/abcde""", post.Text); } } } diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index 38124eb76..54c2efe79 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -52,7 +52,7 @@ private TwitterStatus CreateStatus() IdStr = statusId.ToString(), CreatedAt = "Sat Jan 01 00:00:00 +0000 2022", FullText = "hoge", - Source = "OpenTween", + Source = """OpenTween""", Entities = new(), User = this.CreateUser(), }; @@ -160,7 +160,7 @@ public void CreateFromStatus_RetweetTest() var retweetStatus = this.CreateStatus(); retweetStatus.RetweetedStatus = originalStatus; - retweetStatus.Source = "Twitter Web App"; + retweetStatus.Source = """Twitter Web App"""; var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); @@ -315,7 +315,7 @@ public void CreateFromStatus_MediaAltTest() var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); Assert.Equal(accessibleText, post.AccessibleText); - Assert.Equal("pic.twitter.com/hoge", post.Text); + Assert.Equal("""pic.twitter.com/hoge""", post.Text); Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); } @@ -345,7 +345,7 @@ public void CreateFromStatus_MediaNoAltTest() var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); Assert.Equal("pic.twitter.com/hoge", post.AccessibleText); - Assert.Equal("pic.twitter.com/hoge", post.Text); + Assert.Equal("""pic.twitter.com/hoge""", post.Text); Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); } @@ -387,7 +387,7 @@ public void CreateFromStatus_QuotedUrlTest() var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); - Assert.Equal("twitter.com/hoge/status/1…", post.Text); + Assert.Equal("""twitter.com/hoge/status/1…""", post.Text); Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); } @@ -422,7 +422,7 @@ public void CreateFromStatus_QuotedUrlWithPermelinkTest() var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); - Assert.Equal("hoge twitter.com/hoge/status/1…", post.Text); + Assert.Equal("""hoge twitter.com/hoge/status/1…""", post.Text); Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextSingleLine); } @@ -453,7 +453,7 @@ public void CreateFromStatus_QuotedUrlNoReferenceTest() var accessibleText = "twitter.com/hoge/status/1…"; Assert.Equal(accessibleText, post.AccessibleText); - Assert.Equal("twitter.com/hoge/status/1…", post.Text); + Assert.Equal("""twitter.com/hoge/status/1…""", post.Text); Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); } @@ -484,9 +484,9 @@ public void CreateHtmlAnchor_Test() }, }; - var expectedHtml = @"@twitterapi" - + @" #BreakingMyTwitter" - + @" apps-of-a-feather.com"; + var expectedHtml = """@twitterapi""" + + """ #BreakingMyTwitter""" + + """ apps-of-a-feather.com"""; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); } @@ -497,7 +497,7 @@ public void CreateHtmlAnchor_NicovideoTest() var text = "sm9"; var entities = new TwitterEntities(); - var expectedHtml = @"sm9"; + var expectedHtml = """sm9"""; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); } @@ -515,7 +515,7 @@ public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() }; var expectedHtml = @"hoge" - + @" twitter.com/hoge/status/1…"; + + """ twitter.com/hoge/status/1…"""; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink)); } @@ -523,7 +523,7 @@ public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() [Fact] public void ParseSource_Test() { - var sourceHtml = "Twitter Web Client"; + var sourceHtml = """Twitter Web Client"""; var expected = ("Twitter Web Client", new Uri("http://twitter.com/")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); @@ -542,7 +542,7 @@ public void ParseSource_PlainTextTest() public void ParseSource_RelativeUriTest() { // 参照: https://twitter.com/kim_upsilon/status/477796052049752064 - var sourceHtml = "erased_45416"; + var sourceHtml = """erased_45416"""; var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); @@ -570,7 +570,7 @@ public void ParseSource_NullTest() [Fact] public void ParseSource_UnescapeTest() { - var sourceHtml = "<<hogehoge>>"; + var sourceHtml = """<<hogehoge>>"""; var expected = ("<>", new Uri("http://example.com/?aaa=123&bbb=456")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index d94bada41..71ffbf50b 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -128,7 +128,7 @@ public struct JsonData public static readonly TheoryData CreateDataFromJsonTestCase = new() { { - @"{""id"":""1"", ""body"":""hogehoge""}", + """{"id":"1", "body":"hogehoge"}""", new JsonData { Id = "1", Body = "hogehoge" } }, }; diff --git a/OpenTween.Tests/ShortUrlTest.cs b/OpenTween.Tests/ShortUrlTest.cs index 887222617..b5402176c 100644 --- a/OpenTween.Tests/ShortUrlTest.cs +++ b/OpenTween.Tests/ShortUrlTest.cs @@ -370,8 +370,8 @@ public async Task ExpandUrlHtmlAsync_Test() return this.CreateRedirectResponse("http://example.com/hoge2"); }); - Assert.Equal("hogehoge", - await shortUrl.ExpandUrlHtmlAsync("hogehoge")); + Assert.Equal("""hogehoge""", + await shortUrl.ExpandUrlHtmlAsync("""hogehoge""")); Assert.Equal(0, handler.QueueCount); } @@ -390,8 +390,8 @@ public async Task ExpandUrlHtmlAsync_RelativeUriTest() return this.CreateRedirectResponse("http://example.com/hoge"); }); - Assert.Equal("hogehoge", - await shortUrl.ExpandUrlHtmlAsync("hogehoge")); + Assert.Equal("""hogehoge""", + await shortUrl.ExpandUrlHtmlAsync("""hogehoge""")); Assert.Equal(1, handler.QueueCount); } diff --git a/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs index 6d8bb1f82..37c92c702 100644 --- a/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs @@ -221,23 +221,25 @@ public async Task GetThumbnailInfoAsync_ApiKeyErrorTest() [Fact] public void ParseInLocation_Test() { - var json = @"{ - ""meta"": { ""code"": 200 }, - ""response"": { - ""checkin"": { - ""id"": ""xxxxxxxxx"", - ""type"": ""checkin"", - ""venue"": { - ""id"": ""4b73dedcf964a5206bbe2de3"", - ""name"": ""高松駅 (Takamatsu Sta.)"", - ""location"": { - ""lat"": 34.35067978344854, - ""lng"": 134.04693603515625 - } - } - } - } -}"; + var json = """ + { + "meta": { "code": 200 }, + "response": { + "checkin": { + "id": "xxxxxxxxx", + "type": "checkin", + "venue": { + "id": "4b73dedcf964a5206bbe2de3", + "name": "高松駅 (Takamatsu Sta.)", + "location": { + "lat": 34.35067978344854, + "lng": 134.04693603515625 + } + } + } + } + } + """; var jsonBytes = Encoding.UTF8.GetBytes(json); var location = FoursquareCheckin.ParseIntoLocation(jsonBytes); @@ -249,23 +251,25 @@ public void ParseInLocation_Test() [Fact] public void ParseInLocation_CultureTest() { - var json = @"{ - ""meta"": { ""code"": 200 }, - ""response"": { - ""checkin"": { - ""id"": ""xxxxxxxxx"", - ""type"": ""checkin"", - ""venue"": { - ""id"": ""4b73dedcf964a5206bbe2de3"", - ""name"": ""高松駅 (Takamatsu Sta.)"", - ""location"": { - ""lat"": 34.35067978344854, - ""lng"": 134.04693603515625 - } - } - } - } -}"; + var json = """ + { + "meta": { "code": 200 }, + "response": { + "checkin": { + "id": "xxxxxxxxx", + "type": "checkin", + "venue": { + "id": "4b73dedcf964a5206bbe2de3", + "name": "高松駅 (Takamatsu Sta.)", + "location": { + "lat": 34.35067978344854, + "lng": 134.04693603515625 + } + } + } + } + } + """; var origCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = new CultureInfo("ru-RU"); @@ -282,24 +286,26 @@ public void ParseInLocation_CultureTest() [Fact] public void ParseInLocation_PlanetTest() { - var json = @"{ - ""meta"": { ""code"": 200 }, - ""response"": { - ""checkin"": { - ""id"": ""xxxxxxxxx"", - ""type"": ""checkin"", - ""venue"": { - ""id"": ""5069d8bdc640385aa7711fe4"", - ""name"": ""Gale Crater"", - ""location"": { - ""planet"": ""mars"", - ""lat"": 34.201694, - ""lng"": -118.17166 - } - } - } - } -}"; + var json = """ + { + "meta": { "code": 200 }, + "response": { + "checkin": { + "id": "xxxxxxxxx", + "type": "checkin", + "venue": { + "id": "5069d8bdc640385aa7711fe4", + "name": "Gale Crater", + "location": { + "planet": "mars", + "lat": 34.201694, + "lng": -118.17166 + } + } + } + } + } + """; var jsonBytes = Encoding.UTF8.GetBytes(json); var location = FoursquareCheckin.ParseIntoLocation(jsonBytes); @@ -310,16 +316,18 @@ public void ParseInLocation_PlanetTest() [Fact] public void ParseInLocation_VenueNullTest() { - var json = @"{ - ""meta"": { ""code"": 200 }, - ""response"": { - ""checkin"": { - ""id"": ""xxxxxxxxx"", - ""type"": ""checkin"", - ""venue"": null - } - } -}"; + var json = """ + { + "meta": { "code": 200 }, + "response": { + "checkin": { + "id": "xxxxxxxxx", + "type": "checkin", + "venue": null + } + } + } + """; var jsonBytes = Encoding.UTF8.GetBytes(json); var location = FoursquareCheckin.ParseIntoLocation(jsonBytes); diff --git a/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs b/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs index 6031fbed9..45283518b 100644 --- a/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/ImgAzyobuziNetTest.cs @@ -62,12 +62,12 @@ protected override Task FetchRegexAsync(string apiBase) throw new HttpRequestException(); if (apiBase == "http://error.example.com/api/") - return Encoding.UTF8.GetBytes("{\"error\": {\"code\": 5001}}"); + return Encoding.UTF8.GetBytes("""{"error": {"code": 5001}}"""); if (apiBase == "http://invalid.example.com/api/") return Encoding.UTF8.GetBytes("<<>>"); - return Encoding.UTF8.GetBytes("[{\"name\": \"hogehoge\", \"regex\": \"^https?://example.com/(.+)$\"}]"); + return Encoding.UTF8.GetBytes("""[{"name": "hogehoge", "regex": "^https?://example.com/(.+)$"}]"""); }); } } diff --git a/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs b/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs index 2e30ba675..5b458e5b0 100644 --- a/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/MetaThumbnailServiceTest.cs @@ -54,19 +54,19 @@ public async Task OGPMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - - - - - - hogehoge - - -

hogehoge

- - -"; + service.FakeHtml = """ + + + + + + hogehoge + + +

hogehoge

+ + + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -80,15 +80,15 @@ public async Task TwitterMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - -hogehoge + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -102,15 +102,15 @@ public async Task InvalidMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - -hogehoge + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -124,15 +124,15 @@ public async Task ReverseMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - -hogehoge + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -146,16 +146,16 @@ public async Task BadMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - - -hogehoge + + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -169,15 +169,15 @@ public async Task BadMetaOneLineTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - -hogehoge + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -191,15 +191,15 @@ public async Task ReverseMetaOneLineTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - - -hogehoge + + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -213,14 +213,14 @@ public async Task NoMetaTest() { var service = new TestMetaThumbnailService(@"http://example.com/.+"); - service.FakeHtml = @" - + service.FakeHtml = """ + - -hogehoge + + hogehoge -

hogehoge -"; +

hogehoge + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://example.com/abcd", new PostClass(), CancellationToken.None); Assert.Null(thumbinfo); diff --git a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs index d2d1c9a46..5a6db06fb 100644 --- a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs @@ -56,21 +56,23 @@ public async Task ApiTest() { var service = new TestTinami(); - service.FakeXml = @" - - - ほげほげ - 説明 - - - - - http://img.tinami.com/hogehoge_full.gif - 640 - 480 - - -"; + service.FakeXml = """ + + + + ほげほげ + 説明 + + + + + http://img.tinami.com/hogehoge_full.gif + 640 + 480 + + + + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://www.tinami.com/view/12345", new PostClass(), CancellationToken.None); Assert.NotNull(thumbinfo); @@ -84,10 +86,12 @@ public async Task ApiErrorTest() { var service = new TestTinami(); - service.FakeXml = @" - - -"; + service.FakeXml = """ + + + + + """; var thumbinfo = await service.GetThumbnailInfoAsync("http://www.tinami.com/view/12345", new PostClass(), CancellationToken.None); Assert.Null(thumbinfo); diff --git a/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs b/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs index e31155dce..3ff5aba00 100644 --- a/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs @@ -17,31 +17,33 @@ public class TumblrTest [Fact] public void ParsePostJson_Test() { - var json = @"{ - ""meta"": { ""status"": 200, ""msg"": ""OK"" }, - ""response"": { - ""blog"": { }, - ""posts"": [ - { - ""id"": 1234567, - ""post_url"": ""http://example.com/post/1234567"", - ""type"": ""photo"", - ""photos"": [ - { - ""caption"": """", - ""alt_sizes"": [ - { - ""width"": 1280, - ""height"": 722, - ""url"": ""http://example.com/photo/1280/1234567/1/tumblr_hogehoge"" - } - ] - } - ] - } - ] - } -}"; + var json = """ + { + "meta": { "status": 200, "msg": "OK" }, + "response": { + "blog": { }, + "posts": [ + { + "id": 1234567, + "post_url": "http://example.com/post/1234567", + "type": "photo", + "photos": [ + { + "caption": "", + "alt_sizes": [ + { + "width": 1280, + "height": 722, + "url": "http://example.com/photo/1280/1234567/1/tumblr_hogehoge" + } + ] + } + ] + } + ] + } + } + """; var jsonBytes = Encoding.UTF8.GetBytes(json); var thumbs = Tumblr.ParsePhotoPostJson(jsonBytes); @@ -75,10 +77,12 @@ public async Task GetThumbnailInfoAsync_RequestTest() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(@"{ - ""meta"": { ""status"": 200, ""msg"": ""OK"" }, - ""response"": { ""blog"": { }, ""posts"": { } } -}"), + Content = new StringContent(""" + { + "meta": { "status": 200, "msg": "OK" }, + "response": { "blog": { }, "posts": { } } + } + """), }; }); @@ -112,10 +116,12 @@ public async Task GetThumbnailInfoAsync_CustomHostnameRequestTest() return new HttpResponseMessage(HttpStatusCode.OK) { - Content = new StringContent(@"{ - ""meta"": { ""status"": 200, ""msg"": ""OK"" }, - ""response"": { ""blog"": { }, ""posts"": { } } -}"), + Content = new StringContent(""" + { + "meta": { "status": 200, "msg": "OK" }, + "response": { "blog": { }, "posts": { } } + } + """), }; }); diff --git a/OpenTween.Tests/TweenMainTest.cs b/OpenTween.Tests/TweenMainTest.cs index 275074c1e..a7974618c 100644 --- a/OpenTween.Tests/TweenMainTest.cs +++ b/OpenTween.Tests/TweenMainTest.cs @@ -78,7 +78,7 @@ public void GetUrlFromDataObject_UnknownFormatTest() [Fact] public void CreateRetweetUnofficial_UrlTest() { - var statusText = "twitter.com"; + var statusText = """twitter.com"""; Assert.Equal("http://twitter.com/", TweenMain.CreateRetweetUnofficial(statusText, false)); } @@ -86,7 +86,7 @@ public void CreateRetweetUnofficial_UrlTest() [Fact] public void CreateRetweetUnofficial_MentionTest() { - var statusText = "@TwitterAPI"; + var statusText = """@TwitterAPI"""; Assert.Equal("@TwitterAPI", TweenMain.CreateRetweetUnofficial(statusText, false)); } @@ -94,7 +94,7 @@ public void CreateRetweetUnofficial_MentionTest() [Fact] public void CreateRetweetUnofficial_HashtagTest() { - var statusText = "#OpenTween"; + var statusText = """#OpenTween"""; Assert.Equal("#OpenTween", TweenMain.CreateRetweetUnofficial(statusText, false)); } diff --git a/OpenTween.Tests/TweetDetailsViewTest.cs b/OpenTween.Tests/TweetDetailsViewTest.cs index babbe42b0..6aeceea2c 100644 --- a/OpenTween.Tests/TweetDetailsViewTest.cs +++ b/OpenTween.Tests/TweetDetailsViewTest.cs @@ -40,15 +40,15 @@ public void FormatQuoteTweetHtml_PostClassTest() StatusId = new TwitterStatusId("12345"), Nickname = "upsilon", ScreenName = "kim_upsilon", - Text = "@twitterapi hogehoge", + Text = """@twitterapi hogehoge""", CreatedAt = new DateTimeUtc(2015, 3, 30, 3, 30, 0), }; // PostClass.Text はリンクを除去するのみでエスケープは行わない // (TweetFormatter によって既にエスケープされた文字列が格納されているため) - var expected = "" + - "

" + + var expected = """""" + + """
""" + "

@twitterapi hogehoge

— upsilon (@kim_upsilon) " + DateTimeUtc.Parse("2015/03/30 3:30:00", DateTimeFormatInfo.InvariantInfo).ToLocalTimeString() + "
"; Assert.Equal(expected, TweetDetailsView.FormatQuoteTweetHtml(post, isReply: false)); @@ -60,8 +60,8 @@ public void FormatQuoteTweetHtml_HtmlTest() var statusId = new TwitterStatusId("12345"); // リンク先のステータスID var html = "hogehoge"; // HTMLをそのまま出力する (エスケープしない) - var expected = "" + - "
hogehoge
" + + var expected = """
""" + + """
hogehoge
""" + "
"; Assert.Equal(expected, TweetDetailsView.FormatQuoteTweetHtml(statusId, html, isReply: false)); } @@ -70,8 +70,8 @@ public void FormatQuoteTweetHtml_HtmlTest() public void FormatQuoteTweetHtml_ReplyHtmlTest() { // blockquote の class に reply が付与される - var expected = "" + - "
hogehoge
" + + var expected = """
""" + + """
hogehoge
""" + "
"; Assert.Equal(expected, TweetDetailsView.FormatQuoteTweetHtml(new TwitterStatusId("12345"), "hogehoge", isReply: true)); } @@ -79,7 +79,7 @@ public void FormatQuoteTweetHtml_ReplyHtmlTest() [Fact] public void StripLinkTagHtml_Test() { - var html = "@twitterapi"; + var html = """@twitterapi"""; var expected = "@twitterapi"; Assert.Equal(expected, TweetDetailsView.StripLinkTagHtml(html)); diff --git a/OpenTween.Tests/TweetFormatterTest.cs b/OpenTween.Tests/TweetFormatterTest.cs index 8902c96a5..49a417e1b 100644 --- a/OpenTween.Tests/TweetFormatterTest.cs +++ b/OpenTween.Tests/TweetFormatterTest.cs @@ -46,7 +46,7 @@ public void FormatUrlEntity_Test() }, }; - var expected = "example.com"; + var expected = """example.com"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -66,7 +66,7 @@ public void FormatUrlEntity_TwitterComTest() }; // twitter.com 宛のリンクは t.co を経由せずにリンクする - var expected = "twitter.com/twitterapi"; + var expected = """twitter.com/twitterapi"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -83,7 +83,7 @@ public void FormatHashtagEntity_Test() }, }; - var expected = "#OpenTween"; + var expected = """#OpenTween"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -102,7 +102,7 @@ public void FormatMentionEntity_Test() }, }; - var expected = "@TwitterAPI"; + var expected = """@TwitterAPI"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -131,7 +131,7 @@ public void FormatMediaEntity_Test() }, }; - var expected = "pic.twitter.com/h5dCr4ftN4"; + var expected = """pic.twitter.com/h5dCr4ftN4"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -161,7 +161,7 @@ public void FormatMediaEntity_AltTextTest() }, }; - var expected = "pic.twitter.com/h5dCr4ftN4"; + var expected = """pic.twitter.com/h5dCr4ftN4"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -179,7 +179,7 @@ public void FormatEmojiEntity_Test() }, }; - var expected = "\"🍣\""; + var expected = """🍣"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -254,7 +254,7 @@ public void AutoLinkHtml_EscapeTest() }, }; - var expected = ""'@twitterapi'""; + var expected = """"'@twitterapi'""""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -274,7 +274,7 @@ public void AutoLinkHtml_EscapeTest2() }, }; - var expected = "<b> @twitterapi </b>"; + var expected = """<b> @twitterapi </b>"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -294,7 +294,7 @@ public void AutoLinkHtml_EscapeTest3() }, }; - var expected = "<b> @twitterapi </b>"; + var expected = """<b> @twitterapi </b>"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -312,7 +312,7 @@ public void AutoLinkHtml_EscapeUrlTest() }, }; - var expected = "#ぜんぶ雪のせいだ"; + var expected = """#ぜんぶ雪のせいだ"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -332,7 +332,7 @@ public void AutoLinkHtml_SurrogatePairTest() }, }; - var expected = "🐬🐬 @irucame 🐬🐬"; + var expected = """🐬🐬 @irucame 🐬🐬"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -355,8 +355,8 @@ public void AutoLinkHtml_SurrogatePairTest2() }, }; - var expected = "🐬🐬 #🐬🐬 " + - "🐬🐬 #🐬🐬 🐬🐬"; + var expected = """🐬🐬 #🐬🐬 """ + + """🐬🐬 #🐬🐬 🐬🐬"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -375,7 +375,7 @@ public void AutoLinkHtml_CompositeCharacterTest() }, }; - var expected = "Caf\u00e9 #test"; + var expected = "Caf\u00e9 " + """#test"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -394,7 +394,7 @@ public void AutoLinkHtml_CombiningCharacterSequenceTest() }, }; - var expected = "Cafe\u0301 #test"; + var expected = "Cafe\u0301 " + """#test"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } @@ -462,7 +462,7 @@ public void AutoLinkHtml_OverlappedEntitiesTest() }; var expected = ""I hope you'll keep...building bonds of friendship that will enrich your lives & enrich our world" \u2014FLOTUS in China, " + - "pic.twitter.com/fxmuQN9JL9"; + """pic.twitter.com/fxmuQN9JL9"""; Assert.Equal(expected, TweetFormatter.AutoLinkHtml(text, entities)); } } diff --git a/OpenTween.Tests/TwitterTest.cs b/OpenTween.Tests/TwitterTest.cs index de1cd17c7..56bdf6c52 100644 --- a/OpenTween.Tests/TwitterTest.cs +++ b/OpenTween.Tests/TwitterTest.cs @@ -38,10 +38,10 @@ public class TwitterTest [Theory] [InlineData("https://twitter.com/twitterapi/status/22634515958", new[] { "22634515958" })] - [InlineData("twitter.com/twitterapi/stat…", + [InlineData("""twitter.com/twitterapi/stat…""", new[] { "22634515958" })] - [InlineData("https://t.co/bU3oR95KIy" + - "https://t.co/bbbbbbbb", + [InlineData("""https://t.co/bU3oR95KIy""" + + """https://t.co/bbbbbbbb""", new[] { "224782458816692224", "311081657790771200" })] [InlineData("https://mobile.twitter.com/muji_net/status/21984934471", new[] { "21984934471" })] diff --git a/OpenTween/Api/MicrosoftTranslatorApi.cs b/OpenTween/Api/MicrosoftTranslatorApi.cs index 6400a7ad7..ab5e58b7a 100644 --- a/OpenTween/Api/MicrosoftTranslatorApi.cs +++ b/OpenTween/Api/MicrosoftTranslatorApi.cs @@ -85,7 +85,7 @@ await this.UpdateAccessTokenIfExpired() request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this.AccessToken); var escapedText = JsonUtils.EscapeJsonString(text); - var json = $@"[{{""Text"": ""{escapedText}""}}]"; + var json = $$"""[{"Text": "{{escapedText}}"}]"""; using var body = new StringContent(json, Encoding.UTF8, "application/json"); request.Content = body; diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index e4df794b6..30b8cfb7c 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -445,28 +445,31 @@ public Task> DirectMessagesEventsNew(long re var attachment = ""; if (mediaId != null) { - attachment = "," + $@" - ""attachment"": {{ - ""type"": ""media"", - ""media"": {{ - ""id"": ""{JsonUtils.EscapeJsonString(mediaId.ToString())}"" - }} - }}"; + attachment = ",\r\n" + $$""" + "attachment": { + "type": "media", + "media": { + "id": "{{JsonUtils.EscapeJsonString(mediaId.ToString())}}" + } + } + """; } - var json = $@"{{ - ""event"": {{ - ""type"": ""message_create"", - ""message_create"": {{ - ""target"": {{ - ""recipient_id"": ""{JsonUtils.EscapeJsonString(recipientId.ToString())}"" - }}, - ""message_data"": {{ - ""text"": ""{JsonUtils.EscapeJsonString(text)}""{attachment} - }} - }} - }} -}}"; + var json = $$""" + { + "event": { + "type": "message_create", + "message_create": { + "target": { + "recipient_id": "{{JsonUtils.EscapeJsonString(recipientId.ToString())}}" + }, + "message_data": { + "text": "{{JsonUtils.EscapeJsonString(text)}}"{{attachment}} + } + } + } + } + """; return this.Connection.PostJsonAsync(endpoint, json); } @@ -807,7 +810,7 @@ public Task MediaMetadataCreate(long mediaId, string altText) var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json"); var escapedAltText = JsonUtils.EscapeJsonString(altText); - var json = $@"{{""media_id"": ""{mediaId}"", ""alt_text"": {{""text"": ""{escapedAltText}""}}}}"; + var json = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}"""; return this.Connection.PostJsonAsync(endpoint, json); } diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index 3015a7c47..7e2496769 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -238,7 +238,7 @@ public string SourceHtml return WebUtility.HtmlEncode(this.Source); return string.Format( - "{1}", + """{1}""", WebUtility.HtmlEncode(this.SourceUri.AbsoluteUri), WebUtility.HtmlEncode(this.Source)); } diff --git a/OpenTween/Models/TwitterPostFactory.cs b/OpenTween/Models/TwitterPostFactory.cs index 34ca67fb8..d63677c08 100644 --- a/OpenTween/Models/TwitterPostFactory.cs +++ b/OpenTween/Models/TwitterPostFactory.cs @@ -409,12 +409,12 @@ internal static string CreateHtmlAnchor(string text, TwitterEntities entities, T // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない text = TweetFormatter.AutoLinkHtml(text, mergedEntities, keepTco: true); - text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1$2$3"); + text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", """$1$2$3"""); text = PreProcessUrl(text); // IDN置換 if (quotedStatusLink != null) { - text += string.Format(" {1}", + text += string.Format(""" {1}""", WebUtility.HtmlEncode(quotedStatusLink.Expanded), WebUtility.HtmlEncode(quotedStatusLink.Display)); } @@ -473,7 +473,7 @@ public static (string SourceText, Uri? SourceUri) ParseSource(string? sourceHtml // sourceHtmlの例: Twitter Web Client - var match = Regex.Match(sourceHtml, "^.+?)\".*?>(?.+)$", RegexOptions.IgnoreCase); + var match = Regex.Match(sourceHtml, """^(?.+)$""", RegexOptions.IgnoreCase); if (match.Success) { sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value); diff --git a/OpenTween/Thumbnail/Services/MetaThumbnailService.cs b/OpenTween/Thumbnail/Services/MetaThumbnailService.cs index a45474cb9..d5efd3c52 100644 --- a/OpenTween/Thumbnail/Services/MetaThumbnailService.cs +++ b/OpenTween/Thumbnail/Services/MetaThumbnailService.cs @@ -42,8 +42,8 @@ public class MetaThumbnailService : IThumbnailService { protected static Regex[] metaPatterns = { - new Regex(".+?)[\"'] (content|value)=[\"'](?[^>]+?)[\"']"), - new Regex("[^>]+?)[\"'] (name|property)=[\"'](?.+?)[\"']"), + new Regex(""".+?)["'] (content|value)=["'](?[^>]+?)["']"""), + new Regex("""[^>]+?)["'] (name|property)=["'](?.+?)["']"""), }; protected static string[] defaultPropertyNames = { "og:image", "twitter:image", "twitter:image:src" }; diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 9dd7d350c..842a04655 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -98,10 +98,10 @@ public partial class TweenMain : OTBaseForm private readonly object syncObject = new(); // ロック用 private const string DetailHtmlFormatHead = - "" - + "