From 8d7f8c6714d3bfbb19442df763095507576d0107 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Wed, 7 Feb 2024 10:12:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E3=83=A1=E3=82=A4=E3=83=B3=E4=BB=A5?= =?UTF-8?q?=E5=A4=96=E3=81=AE=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=83=9B=E3=83=BC=E3=83=A0=E3=82=BF=E3=82=A4=E3=83=A0?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99?= =?UTF-8?q?=E3=82=8B=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.txt | 4 + .../SocialProtocol/AccountCollectionTest.cs | 19 +++++ OpenTween/ApplicationEvents.cs | 16 ++++ .../Models/HomeSpecifiedAccountTabModel.cs | 74 +++++++++++++++++++ OpenTween/SocialProtocol/AccountCollection.cs | 3 + OpenTween/Tween.cs | 30 +++++++- 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 OpenTween/Models/HomeSpecifiedAccountTabModel.cs diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3707ae3b3..0657dd64a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,10 @@ 更新履歴 ==== Unreleased + * NEW: メインアカウント以外のホームタイムライン表示に対応 + - タブ単位で切り替わるマルチアカウント機能です + - 投稿欄やふぁぼ・RT等の機能も表示中のタブに連動して使用するアカウントが変わります + - 現時点ではメインアカウント以外のタブ設定は次回起動時に保持されません * NEW: Twemoji 15.1.0 に対応しました - Unicode 15.1 で追加された絵文字が表示されるようになります * CHG: 設定画面でのアカウント一覧の表示形式を変更 diff --git a/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs b/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs index 7ba90e6cb..ae35efc0e 100644 --- a/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs +++ b/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs @@ -141,6 +141,25 @@ public void LoadFromSettings_ReconfigureTest() Assert.False(accountItem2.IsDisposed); } + [Fact] + public void SecondaryAccounts_Test() + { + using var accounts = new AccountCollection(); + accounts.LoadFromSettings(new() + { + UserAccounts = new() + { + this.CreateAccountSetting("00000000-0000-4000-8000-000000000000"), + this.CreateAccountSetting("00000000-0000-4000-8000-111111111111"), + }, + SelectedAccountKey = new("00000000-0000-4000-8000-000000000000"), + }); + + var secondaryAccounts = accounts.SecondaryAccounts; + Assert.Single(secondaryAccounts); + Assert.Equal(new("00000000-0000-4000-8000-111111111111"), secondaryAccounts[0].UniqueKey); + } + [Fact] public void GetAccountForTab_DefaultTest() { diff --git a/OpenTween/ApplicationEvents.cs b/OpenTween/ApplicationEvents.cs index 18f7986e9..dd802ac8f 100644 --- a/OpenTween/ApplicationEvents.cs +++ b/OpenTween/ApplicationEvents.cs @@ -31,6 +31,7 @@ using System.Linq; using System.Windows.Forms; using OpenTween.Connection; +using OpenTween.Models; using OpenTween.Setting; using OpenTween.SocialProtocol; using OpenTween.SocialProtocol.Twitter; @@ -100,6 +101,7 @@ public static int Main(string[] args) } SetupAccounts(container.AccountCollection, settings); + AddSecondaryAccountTabs(container.AccountCollection, container.TabInfo); Application.Run(container.MainForm); @@ -176,5 +178,19 @@ private static void VerifyCredentialsSync(ISocialAccount account) throw new WebApiException(ex.InnerException.Message, ex); } } + + private static void AddSecondaryAccountTabs(AccountCollection accounts, TabInformations tabInfo) + { + foreach (var account in accounts.Items) + { + var accountKey = account.UniqueKey; + if (accountKey == accounts.Primary.UniqueKey) + continue; + + var tabName = tabInfo.MakeTabName($"@{account.UserName}"); + var homeTab = new HomeSpecifiedAccountTabModel(tabName, accountKey); + tabInfo.AddTab(homeTab); + } + } } } diff --git a/OpenTween/Models/HomeSpecifiedAccountTabModel.cs b/OpenTween/Models/HomeSpecifiedAccountTabModel.cs new file mode 100644 index 000000000..cbe10f8fa --- /dev/null +++ b/OpenTween/Models/HomeSpecifiedAccountTabModel.cs @@ -0,0 +1,74 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.Threading.Tasks; +using OpenTween.SocialProtocol; + +namespace OpenTween.Models +{ + public class HomeSpecifiedAccountTabModel : InternalStorageTabModel + { + public override MyCommon.TabUsageType TabType + => MyCommon.TabUsageType.Undefined; + + public override bool IsPermanentTabType + => false; + + public override Guid? SourceAccountId { get; } + + public HomeSpecifiedAccountTabModel(string tabName, Guid accountId) + : base(tabName) + { + this.SourceAccountId = accountId; + } + + public override async Task RefreshAsync(ISocialAccount account, bool backward, IProgress progress) + { + progress.Report("Home refreshing..."); + + var firstLoad = !this.IsFirstLoadCompleted; + var count = Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, backward, firstLoad); + var cursor = backward ? this.CursorBottom : this.CursorTop; + + var response = await account.Client.GetHomeTimeline(count, cursor, firstLoad) + .ConfigureAwait(false); + + foreach (var post in response.Posts) + this.AddPostQueue(post); + + TabInformations.GetInstance().DistributePosts(); + + if (response.CursorTop != null && !backward) + this.CursorTop = response.CursorTop; + + if (response.CursorBottom != null) + this.CursorBottom = response.CursorBottom; + + if (firstLoad) + this.IsFirstLoadCompleted = true; + + progress.Report("Home refreshed"); + } + } +} diff --git a/OpenTween/SocialProtocol/AccountCollection.cs b/OpenTween/SocialProtocol/AccountCollection.cs index cf713802e..afebef2fc 100644 --- a/OpenTween/SocialProtocol/AccountCollection.cs +++ b/OpenTween/SocialProtocol/AccountCollection.cs @@ -43,6 +43,9 @@ public ISocialAccount Primary public ISocialAccount[] Items => this.accounts.Values.ToArray(); + public ISocialAccount[] SecondaryAccounts + => this.accounts.Values.Where(x => x.UniqueKey != this.primaryId).ToArray(); + public void LoadFromSettings(SettingCommon settingCommon) { var oldAccounts = this.accounts; diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 2603383db..a94b0944a 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -509,7 +509,11 @@ ThumbnailGenerator thumbGenerator // タイマー設定 - this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => this.RefreshTabAsync()); + this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => Task.WhenAll(new[] + { + this.RefreshTabAsync(), + this.RefreshTabAsync(), + })); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Mention] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Dm] = () => this.InvokeAsync(() => this.RefreshTabAsync()); this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.PublicSearch] = () => this.InvokeAsync(() => this.RefreshTabAsync()); @@ -2492,6 +2496,7 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) { // 設定画面表示前のユーザー情報 var previousAccountId = this.settings.Common.SelectedAccountKey; + var previousSecondaryAccounts = this.accounts.SecondaryAccounts; var oldIconCol = this.Use2ColumnsMode; if (this.ShowSettingDialog() == DialogResult.OK) @@ -2685,6 +2690,28 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) if (this.PrimaryAccount.UniqueKey != previousAccountId) await this.DoGetFollowersMenu(); + + var currentSecondaryAccounts = this.accounts.SecondaryAccounts; + var newSecondaryAccounts = currentSecondaryAccounts + .Where(x => !previousSecondaryAccounts.Any(y => y.UniqueKey == x.UniqueKey)); + this.AddSecondaryAccountTabs(newSecondaryAccounts); + } + + private void AddSecondaryAccountTabs(IEnumerable accounts) + { + foreach (var account in accounts) + { + var isPrimary = account.UniqueKey == this.accounts.Primary.UniqueKey; + var tabExists = this.statuses.GetTabsByType() + .Any(x => x.SourceAccountId == account.UniqueKey); + if (tabExists) + continue; + + var tabName = this.statuses.MakeTabName($"@{account.UserName}"); + var homeTab = new HomeSpecifiedAccountTabModel(tabName, account.UniqueKey); + this.statuses.AddTab(homeTab); + this.AddNewTab(homeTab, startup: false); + } } /// @@ -7802,6 +7829,7 @@ private async void TweenMain_Shown(object sender, EventArgs e) this.RefreshNoRetweetIdsAsync, this.RefreshTwitterConfigurationAsync, this.RefreshTabAsync, + this.RefreshTabAsync, this.RefreshTabAsync, this.RefreshTabAsync, this.RefreshTabAsync, From a73d8b8c7ac2d457230c8ef5d409b529bac5d46c Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 3 May 2024 00:42:32 +0900 Subject: [PATCH 2/3] =?UTF-8?q?HomeSpecifiedAccountTab=E3=81=AE=E3=82=A2?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E5=90=8D=E3=82=92=E3=82=BF?= =?UTF-8?q?=E3=83=96=E3=81=AE=E3=83=98=E3=83=83=E3=83=80=E3=83=BC=E3=81=AB?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween/Tween.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index a94b0944a..3748b462b 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -2896,6 +2896,15 @@ public bool AddNewTab(TabModel tab, bool startup) HeaderText = listTab.ListInfo.ToString(), }; } + else if (tab is HomeSpecifiedAccountTabModel homeSecondaryTab) + { + var account = this.accounts.GetAccountForTab(homeSecondaryTab); + headerPanel = new GeneralTimelineHeaderPanel + { + Dock = DockStyle.Top, + HeaderText = account != null ? $"@{account.UserName}: Home" : "", + }; + } // 検索関連の準備 else if (tab is PublicSearchTabModel searchTab) { @@ -3021,7 +3030,8 @@ public bool RemoveSpecifiedTab(string tabName, bool confirm) // 後付けのコントロールを破棄 if (tabInfo.TabType == MyCommon.TabUsageType.UserTimeline || - tabInfo.TabType == MyCommon.TabUsageType.Lists) + tabInfo.TabType == MyCommon.TabUsageType.Lists || + tabInfo is HomeSpecifiedAccountTabModel) { using var panel = tabPage.Controls.OfType().First(); tabPage.Controls.Remove(panel); From 616b078fb7668df526708dab1fbd3d16d1f23bf3 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Fri, 3 May 2024 01:26:53 +0900 Subject: [PATCH 3/3] =?UTF-8?q?GetAccountForTab=E3=81=A7=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E3=81=8C=E4=B8=8D=E6=98=8E=E3=81=AA?= =?UTF-8?q?=E5=A0=B4=E5=90=88=E3=81=ABnull=E3=81=AE=E4=BB=A3=E3=82=8F?= =?UTF-8?q?=E3=82=8A=E3=81=ABInvalidAccount=E3=82=A4=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=82=B9=E3=82=92=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SocialProtocol/AccountCollectionTest.cs | 12 +- .../SocialProtocol/InvalidAccountTest.cs | 52 +++++++++ OpenTween/SocialProtocol/AccountCollection.cs | 5 +- OpenTween/SocialProtocol/InvalidAccount.cs | 106 ++++++++++++++++++ OpenTween/Tween.cs | 6 +- 5 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 OpenTween.Tests/SocialProtocol/InvalidAccountTest.cs create mode 100644 OpenTween/SocialProtocol/InvalidAccount.cs diff --git a/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs b/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs index ae35efc0e..28f9a16a1 100644 --- a/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs +++ b/OpenTween.Tests/SocialProtocol/AccountCollectionTest.cs @@ -177,7 +177,8 @@ public void GetAccountForTab_DefaultTest() // SourceAccountId が null のタブに対しては Primary のアカウントを返す var actual = accounts.GetAccountForTab(tabWithoutAccountId); - Assert.Equal(new("00000000-0000-4000-8000-000000000000"), actual?.UniqueKey); + Assert.IsType(actual); + Assert.Equal(new("00000000-0000-4000-8000-000000000000"), actual.UniqueKey); } [Fact] @@ -198,7 +199,8 @@ public void GetAccountForTab_SpecifiedAccountTest() // SourceAccountId が設定されているタブに対しては対応するアカウントを返す var actual = accounts.GetAccountForTab(tabWithAccountId); - Assert.Equal(new("00000000-0000-4000-8000-111111111111"), actual?.UniqueKey); + Assert.IsType(actual); + Assert.Equal(new("00000000-0000-4000-8000-111111111111"), actual.UniqueKey); } [Fact] @@ -216,8 +218,10 @@ public void GetAccountForTab_NotExistsTest() var tabWithAccountId = new RelatedPostsTabModel("hoge", new("00000000-0000-4000-8000-999999999999"), new()); - // SourceAccountId に存在しない ID が設定されていた場合は null を返す - Assert.Null(accounts.GetAccountForTab(tabWithAccountId)); + // SourceAccountId に存在しない ID が設定されていた場合は InvalidAccount を返す + var actual = accounts.GetAccountForTab(tabWithAccountId); + Assert.IsType(actual); + Assert.Equal(new("00000000-0000-4000-8000-999999999999"), actual.UniqueKey); } } } diff --git a/OpenTween.Tests/SocialProtocol/InvalidAccountTest.cs b/OpenTween.Tests/SocialProtocol/InvalidAccountTest.cs new file mode 100644 index 000000000..4b12eff97 --- /dev/null +++ b/OpenTween.Tests/SocialProtocol/InvalidAccountTest.cs @@ -0,0 +1,52 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.Threading.Tasks; +using OpenTween.Connection; +using Xunit; + +namespace OpenTween.SocialProtocol +{ + public class InvalidAccountTest + { + [Fact] + public async Task Connection_Test() + { + using var account = new InvalidAccount(Guid.NewGuid()); + var requeset = new GetRequest { RequestUri = new("https://example.com/aaa") }; + + await Assert.ThrowsAsync( + () => account.Connection.SendAsync(requeset) + ); + } + + [Fact] + public async Task Client_Test() + { + using var account = new InvalidAccount(Guid.NewGuid()); + + await Assert.ThrowsAsync( + () => account.Client.VerifyCredentials() + ); + } + } +} diff --git a/OpenTween/SocialProtocol/AccountCollection.cs b/OpenTween/SocialProtocol/AccountCollection.cs index afebef2fc..00f1135d2 100644 --- a/OpenTween/SocialProtocol/AccountCollection.cs +++ b/OpenTween/SocialProtocol/AccountCollection.cs @@ -88,14 +88,15 @@ private void DisposeAccounts(IEnumerable accounts) account.Dispose(); } - public ISocialAccount? GetAccountForTab(TabModel tab) + public ISocialAccount GetAccountForTab(TabModel tab) { if (tab.SourceAccountId is { } accountId) { if (this.accounts.TryGetValue(accountId, out var account)) return account; - return null; + // タブ追加後に設定画面からアカウントの情報を削除した場合 + return new InvalidAccount(accountId); } return this.Primary; diff --git a/OpenTween/SocialProtocol/InvalidAccount.cs b/OpenTween/SocialProtocol/InvalidAccount.cs new file mode 100644 index 000000000..5a1dbb4a3 --- /dev/null +++ b/OpenTween/SocialProtocol/InvalidAccount.cs @@ -0,0 +1,106 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2024 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.Threading.Tasks; +using OpenTween.Connection; +using OpenTween.Models; + +namespace OpenTween.SocialProtocol +{ + public class InvalidAccount : ISocialAccount + { + public string AccountType + => "InvalidAccount"; + + public Guid UniqueKey { get; } + + public PersonId UserId + => new TwitterUserId("0"); + + public string UserName + => "(Unknown account)"; + + public IApiConnection Connection { get; } = new InvalidAccountConnection(); + + public ISocialProtocolClient Client { get; } = new InvalidAccountClient(); + + public bool IsDisposed { get; private set; } + + public InvalidAccount(Guid uniqueKey) + => this.UniqueKey = uniqueKey; + + public void Initialize(UserAccount accountSettings, SettingCommon settingCommon) + { + } + + public void Dispose() + => this.IsDisposed = true; + + private class InvalidAccountConnection : IApiConnection + { + public Task SendAsync(IHttpRequest request) + => throw new WebApiException("Invalid account"); + + public void Dispose() + { + } + } + + private class InvalidAccountClient : ISocialProtocolClient + { + public Task VerifyCredentials() + => throw this.CreateException(); + + public Task GetPostById(PostId postId, bool firstLoad) + => throw this.CreateException(); + + public Task GetHomeTimeline(int count, IQueryCursor? cursor, bool firstLoad) + => throw this.CreateException(); + + public Task GetSearchTimeline(string query, string lang, int count, IQueryCursor? cursor, bool firstLoad) + => throw this.CreateException(); + + public Task GetRelatedPosts(PostClass targetPost, bool firstLoad) + => throw this.CreateException(); + + public Task DeletePost(PostId postId) + => throw this.CreateException(); + + public Task FavoritePost(PostId postId) + => throw this.CreateException(); + + public Task UnfavoritePost(PostId postId) + => throw this.CreateException(); + + public Task RetweetPost(PostId postId) + => throw this.CreateException(); + + public Task UnretweetPost(PostId postId) + => throw this.CreateException(); + + private WebApiException CreateException() + => new("Invalid account"); + } + } +} diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 3748b462b..6cb5890a6 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -122,7 +122,7 @@ private ISocialAccount PrimaryAccount => this.accounts.Primary; public ISocialAccount CurrentTabAccount - => this.accounts.GetAccountForTab(this.CurrentTab) ?? throw new InvalidOperationException("Account not found"); + => this.accounts.GetAccountForTab(this.CurrentTab); // Growl呼び出し部 private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName); @@ -1310,7 +1310,7 @@ private async Task RefreshTabAsync(TabModel tab, bool backward) try { var accountForTab = this.accounts.GetAccountForTab(tab); - if (accountForTab == null) + if (accountForTab is InvalidAccount) return; this.RefreshTasktrayIcon(); @@ -2902,7 +2902,7 @@ public bool AddNewTab(TabModel tab, bool startup) headerPanel = new GeneralTimelineHeaderPanel { Dock = DockStyle.Top, - HeaderText = account != null ? $"@{account.UserName}: Home" : "", + HeaderText = $"@{account.UserName}: Home", }; } // 検索関連の準備