From 5f84b1a20befc2a0a033ccd441b9c9326ca34453 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Sun, 7 Jan 2024 03:22:08 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=B3=20v3.11.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 --- CHANGELOG.txt | 2 ++ OpenTween/Properties/AssemblyInfo.cs | 2 +- OpenTween/Properties/Resources.Designer.cs | 4 +++- appveyor.yml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 57496d110..a1e4333e8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ 更新履歴 +==== Unreleased + ==== Ver 3.11.0(2024/01/07) * NEW: Cookie使用時の関連発言表示に対応 * FIX: APIリクエストのタイムアウト時に接続が切断されない場合がある不具合を修正 diff --git a/OpenTween/Properties/AssemblyInfo.cs b/OpenTween/Properties/AssemblyInfo.cs index f2841c999..8721b00d9 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.11.0.0")] +[assembly: AssemblyVersion("3.11.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 7a466ce67..7615947b6 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.11.0(2024/01/07) /// * NEW: Cookie使用時の関連発言表示に対応 /// * FIX: APIリクエストのタイムアウト時に接続が切断されない場合がある不具合を修正 @@ -595,7 +597,7 @@ internal static string ChangeIconToolStripMenuItem_Confirm { ///==== Ver 3.9.0(2023/12/03) /// * NEW: graphqlエンドポイントに対するレートリミットの表示に対応 /// * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更 - /// * FIX: 設定したタイムアウト時間を超えてAPI接続が持続する場合がある不 [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 + /// * FIX: 設定したタイムアウト時間を [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 /// internal static string ChangeLog { get { diff --git a/appveyor.yml b/appveyor.yml index b60cfa1cd..001f7c2fe 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.10.1.{build} +version: 3.11.0.{build} os: Visual Studio 2022 From 0f418ce494b6dcaa4687ece4bcca0cdf8b3ab12a Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 Jan 2024 04:15:44 +0900 Subject: [PATCH 02/16] =?UTF-8?q?DetailsHtmlBuilder=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/DetailsHtmlBuilderTest.cs | 45 +++++++++++ OpenTween/Models/DetailsHtmlBuilder.cs | 77 +++++++++++++++++++ OpenTween/Tween.cs | 49 ++---------- OpenTween/TweetDetailsView.cs | 19 +++-- OpenTween/UserInfoDialog.cs | 13 ++-- 5 files changed, 147 insertions(+), 56 deletions(-) create mode 100644 OpenTween.Tests/Models/DetailsHtmlBuilderTest.cs create mode 100644 OpenTween/Models/DetailsHtmlBuilder.cs diff --git a/OpenTween.Tests/Models/DetailsHtmlBuilderTest.cs b/OpenTween.Tests/Models/DetailsHtmlBuilderTest.cs new file mode 100644 index 000000000..6ba0287e9 --- /dev/null +++ b/OpenTween.Tests/Models/DetailsHtmlBuilderTest.cs @@ -0,0 +1,45 @@ +// 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.Xml.Linq; +using System.Xml.XPath; +using Xunit; + +namespace OpenTween.Models +{ + public class DetailsHtmlBuilderTest + { + [Fact] + public void Build_Test() + { + var settingCommon = new SettingCommon(); + var settingLocal = new SettingLocal(); + using var theme = new ThemeManager(settingLocal); + + var builder = new DetailsHtmlBuilder(); + builder.Prepare(settingCommon, theme); + + var actualHtml = builder.Build("tetete"); + var parsedDocument = XDocument.Parse(actualHtml); + Assert.Equal("tetete", parsedDocument.XPathSelectElement("/html/body/p").Value); + } + } +} diff --git a/OpenTween/Models/DetailsHtmlBuilder.cs b/OpenTween/Models/DetailsHtmlBuilder.cs new file mode 100644 index 000000000..a0219b9e3 --- /dev/null +++ b/OpenTween/Models/DetailsHtmlBuilder.cs @@ -0,0 +1,77 @@ +// 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.Drawing; + +namespace OpenTween.Models +{ + public class DetailsHtmlBuilder + { + private const string TemplateHead = + """""" + + """" + + ""; + + private const string TemplateMonospaced = + $"{TemplateHead}
%CONTENT_HTML%
"; + + private const string TemplateProportional = + $"{TemplateHead}

%CONTENT_HTML%

"; + + private string? preparedTemplate = null; + + public void Prepare(SettingCommon settingCommon, ThemeManager theme) + { + var htmlTemplate = settingCommon.IsMonospace ? TemplateMonospaced : TemplateProportional; + + static string ColorToRGBString(Color color) + => $"{color.R},{color.G},{color.B}"; + + this.preparedTemplate = htmlTemplate + .Replace("%FONT_FAMILY%", theme.FontDetail.Name) + .Replace("%FONT_SIZE%", theme.FontDetail.Size.ToString()) + .Replace("%FONT_COLOR%", ColorToRGBString(theme.ColorDetail)) + .Replace("%LINK_COLOR%", ColorToRGBString(theme.ColorDetailLink)) + .Replace("%BG_COLOR%", ColorToRGBString(theme.ColorDetailBackcolor)) + .Replace("%BG_REPLY_COLOR%", ColorToRGBString(theme.ColorAtTo)); + } + + public string Build(string contentHtml) + { + if (this.preparedTemplate == null) + throw new InvalidOperationException("Template is not prepared."); + + return this.preparedTemplate.Replace("%CONTENT_HTML%", contentHtml); + } + } +} diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index ae3c66027..7124676b2 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -98,27 +98,7 @@ public partial class TweenMain : OTBaseForm private readonly object syncObject = new(); // ロック用 - private const string DetailHtmlFormatHead = - """""" - + """" - + ""; - - private const string DetailHtmlFormatTemplateMono = - $"{DetailHtmlFormatHead}
%CONTENT_HTML%
"; - - private const string DetailHtmlFormatTemplateNormal = - $"{DetailHtmlFormatHead}

%CONTENT_HTML%

"; - - private string detailHtmlFormatPreparedTemplate = null!; + private readonly DetailsHtmlBuilder detailsHtmlBuilder = new(); private bool myStatusError = false; private bool myStatusOnline = false; @@ -356,13 +336,13 @@ ThumbnailGenerator thumbGenerator // フォント&文字色&背景色保持 this.themeManager = new(this.settings.Local); - this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager); + this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager, this.detailsHtmlBuilder); // StringFormatオブジェクトへの事前設定 this.sfTab.Alignment = StringAlignment.Center; this.sfTab.LineAlignment = StringAlignment.Center; - this.InitDetailHtmlFormat(); + this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager); this.tweetDetailsView.ClearPostBrowser(); this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]"; @@ -754,22 +734,6 @@ private void InitColumnText() } } - private void InitDetailHtmlFormat() - { - var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal; - - static string ColorToRGBString(Color color) - => $"{color.R},{color.G},{color.B}"; - - this.detailHtmlFormatPreparedTemplate = htmlTemplate - .Replace("%FONT_FAMILY%", this.themeManager.FontDetail.Name) - .Replace("%FONT_SIZE%", this.themeManager.FontDetail.Size.ToString()) - .Replace("%FONT_COLOR%", ColorToRGBString(this.themeManager.ColorDetail)) - .Replace("%LINK_COLOR%", ColorToRGBString(this.themeManager.ColorDetailLink)) - .Replace("%BG_COLOR%", ColorToRGBString(this.themeManager.ColorDetailBackcolor)) - .Replace("%BG_REPLY_COLOR%", ColorToRGBString(this.themeManager.ColorAtTo)); - } - private void ListTab_DrawItem(object sender, DrawItemEventArgs e) { string txt; @@ -2652,7 +2616,7 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) try { - this.InitDetailHtmlFormat(); + this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager); } catch (Exception ex) { @@ -4162,9 +4126,6 @@ private void UpdateSelectedPost() this.DispSelectedPost(); } - public string CreateDetailHtml(string orgdata) - => this.detailHtmlFormatPreparedTemplate.Replace("%CONTENT_HTML%", orgdata); - private void DispSelectedPost() => this.DispSelectedPost(false); @@ -9006,7 +8967,7 @@ private async Task DoShowUserStatus(string id, bool showInputDialog) private async Task DoShowUserStatus(TwitterUser user) { - using var userDialog = new UserInfoDialog(this, this.tw.Api); + using var userDialog = new UserInfoDialog(this, this.tw.Api, this.detailsHtmlBuilder); var showUserTask = userDialog.ShowUserAsync(user); userDialog.ShowDialog(this); diff --git a/OpenTween/TweetDetailsView.cs b/OpenTween/TweetDetailsView.cs index 4bbe6184d..d0d90970e 100644 --- a/OpenTween/TweetDetailsView.cs +++ b/OpenTween/TweetDetailsView.cs @@ -54,6 +54,9 @@ private TweenMain Owner private ImageCache IconCache => this.iconCache ?? throw this.NotInitializedException(); + private DetailsHtmlBuilder HtmlBuilder + => this.detailsHtmlBuilder ?? throw this.NotInitializedException(); + /// のダンプを表示するか [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] @@ -86,6 +89,7 @@ public ThemeManager Theme private TweenMain? owner; private ImageCache? iconCache; private ThemeManager? themeManager; + private DetailsHtmlBuilder? detailsHtmlBuilder; public TweetDetailsView() { @@ -102,18 +106,19 @@ public TweetDetailsView() this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する } - public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager) + public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager, DetailsHtmlBuilder detailsHtmlBuilder) { this.owner = owner; this.iconCache = iconCache; this.themeManager = themeManager; + this.detailsHtmlBuilder = detailsHtmlBuilder; } private Exception NotInitializedException() => new InvalidOperationException("Cannot call before initialization"); public void ClearPostBrowser() - => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(""); + => this.PostBrowser.DocumentText = this.HtmlBuilder.Build(""); public async Task ShowPostDetails(PostClass post) { @@ -208,14 +213,14 @@ public async Task ShowPostDetails(PostClass post) } sb.Append("-----End PostClass Dump
"); - this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString()); + this.PostBrowser.DocumentText = this.HtmlBuilder.Build(sb.ToString()); return; } using (ControlTransaction.Update(this.PostBrowser)) { this.PostBrowser.DocumentText = - this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text); + this.HtmlBuilder.Build(post.IsDeleted ? "(DELETED)" : post.Text); this.PostBrowser.Document.Window.ScrollTo(0, 0); } @@ -331,7 +336,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml; using (ControlTransaction.Update(this.PostBrowser)) - this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body); + this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body); // 引用ツイートを読み込み var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList(); @@ -348,7 +353,7 @@ private async Task AppendQuoteTweetAsync(PostClass post) body = post.Text + string.Concat(quoteHtmls); using (ControlTransaction.Update(this.PostBrowser)) - this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body); + this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body); } private async Task CreateQuoteTweetHtml(PostId statusId, bool isReply) @@ -422,7 +427,7 @@ private async Task DoTranslation(string str) langFrom: null, langTo: SettingManager.Instance.Common.TranslateLanguage); - this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText); + this.PostBrowser.DocumentText = this.HtmlBuilder.Build(translatedText); } catch (WebApiException e) { diff --git a/OpenTween/UserInfoDialog.cs b/OpenTween/UserInfoDialog.cs index 3a98320d1..e8c06e9cd 100644 --- a/OpenTween/UserInfoDialog.cs +++ b/OpenTween/UserInfoDialog.cs @@ -44,6 +44,7 @@ using OpenTween.Api; using OpenTween.Api.DataModel; using OpenTween.Connection; +using OpenTween.Models; namespace OpenTween { @@ -54,11 +55,13 @@ public partial class UserInfoDialog : OTBaseForm private readonly TweenMain mainForm; private readonly TwitterApi twitterApi; + private readonly DetailsHtmlBuilder detailsHtmlBuilder; - public UserInfoDialog(TweenMain mainForm, TwitterApi twitterApi) + public UserInfoDialog(TweenMain mainForm, TwitterApi twitterApi, DetailsHtmlBuilder detailsHtmlBuilder) { this.mainForm = mainForm; this.twitterApi = twitterApi; + this.detailsHtmlBuilder = detailsHtmlBuilder; this.InitializeComponent(); @@ -183,7 +186,7 @@ private async Task SetDescriptionAsync(string? descriptionText, TwitterEntities? .Concat(TweetExtractor.ExtractEmojiEntities(descriptionText)); var html = TweetFormatter.AutoLinkHtml(descriptionText, mergedEntities); - html = this.mainForm.CreateDetailHtml(html); + html = this.detailsHtmlBuilder.Build(html); if (cancellationToken.IsCancellationRequested) return; @@ -192,7 +195,7 @@ private async Task SetDescriptionAsync(string? descriptionText, TwitterEntities? } else { - this.DescriptionBrowser.DocumentText = this.mainForm.CreateDetailHtml(""); + this.DescriptionBrowser.DocumentText = this.detailsHtmlBuilder.Build(""); } } @@ -263,7 +266,7 @@ private async Task SetRecentStatusAsync(TwitterStatus? status, CancellationToken var mergedEntities = entities.Concat(TweetExtractor.ExtractEmojiEntities(status.FullText)); var html = TweetFormatter.AutoLinkHtml(status.FullText, mergedEntities); - html = this.mainForm.CreateDetailHtml(html + + html = this.detailsHtmlBuilder.Build(html + " Posted at " + MyCommon.DateTimeParse(status.CreatedAt).ToLocalTimeString() + " via " + status.Source); @@ -274,7 +277,7 @@ private async Task SetRecentStatusAsync(TwitterStatus? status, CancellationToken } else { - this.RecentPostBrowser.DocumentText = this.mainForm.CreateDetailHtml(Properties.Resources.ShowUserInfo2); + this.RecentPostBrowser.DocumentText = this.detailsHtmlBuilder.Build(Properties.Resources.ShowUserInfo2); } } From 9dd3bb74f69e7cd1fcc87b68eb7a4bc3e7ebc193 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 Jan 2024 06:17:22 +0900 Subject: [PATCH 03/16] =?UTF-8?q?StatusTextHistory=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/StatusTextHistoryTest.cs | 115 ++++++++++++++++++ OpenTween/Models/StatusTextHistory.cs | 88 ++++++++++++++ OpenTween/Tween.cs | 39 ++---- 3 files changed, 210 insertions(+), 32 deletions(-) create mode 100644 OpenTween.Tests/Models/StatusTextHistoryTest.cs create mode 100644 OpenTween/Models/StatusTextHistory.cs diff --git a/OpenTween.Tests/Models/StatusTextHistoryTest.cs b/OpenTween.Tests/Models/StatusTextHistoryTest.cs new file mode 100644 index 000000000..922522c6a --- /dev/null +++ b/OpenTween.Tests/Models/StatusTextHistoryTest.cs @@ -0,0 +1,115 @@ +// 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 Xunit; + +namespace OpenTween.Models +{ + public class StatusTextHistoryTest + { + [Fact] + public void Initialize_Test() + { + var history = new StatusTextHistory(); + Assert.Single(history.Items); + Assert.Equal(new("", null), history.Items[0]); + Assert.Equal(0, history.HistoryIndex); + } + + [Fact] + public void Back_NoItemsTest() + { + var history = new StatusTextHistory(); + history.Back("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + Assert.Single(history.Items); + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(0, history.HistoryIndex); + } + + [Fact] + public void Back_HasItemsTest() + { + var history = new StatusTextHistory(); + history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.AddLast(); + history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); + + Assert.Equal(2, history.Items.Count); + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(new("@foo bbb", (new TwitterStatusId("222"), "foo")), history.Items[1]); + Assert.Equal(0, history.HistoryIndex); + } + + [Fact] + public void Forward_NoItemsTest() + { + var history = new StatusTextHistory(); + history.Forward("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + Assert.Single(history.Items); + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(0, history.HistoryIndex); + } + + [Fact] + public void Forward_HasItemsTest() + { + var history = new StatusTextHistory(); + history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.AddLast(); + history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); + history.Forward("@hoge aaa 123", (new TwitterStatusId("111"), "hoge")); + + Assert.Equal(2, history.Items.Count); + Assert.Equal(new("@hoge aaa 123", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(new("@foo bbb", (new TwitterStatusId("222"), "foo")), history.Items[1]); + Assert.Equal(1, history.HistoryIndex); + } + + [Fact] + public void AddLast_Test() + { + var history = new StatusTextHistory(); + history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.AddLast(); + Assert.Equal(2, history.Items.Count); + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(new("", null), history.Items[1]); + Assert.Equal(1, history.HistoryIndex); + } + + [Fact] + public void Peek_EmptyTest() + { + var history = new StatusTextHistory(); + Assert.Null(history.Peek()); + } + + [Fact] + public void Peek_HasItemsTest() + { + var history = new StatusTextHistory(); + history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.AddLast(); + + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Peek()); + } + } +} diff --git a/OpenTween/Models/StatusTextHistory.cs b/OpenTween/Models/StatusTextHistory.cs new file mode 100644 index 000000000..ec1d8fe24 --- /dev/null +++ b/OpenTween/Models/StatusTextHistory.cs @@ -0,0 +1,88 @@ +// 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.Collections.Generic; + +namespace OpenTween.Models +{ + public class StatusTextHistory + { + public readonly record struct HistoryItem( + string Status, + (PostId StatusId, string ScreenName)? InReplyTo = null + ); + + internal IReadOnlyList Items + => this.items; + + internal int HistoryIndex + => this.historyIndex; + + private readonly List items = new(); + private int historyIndex = 0; + + public StatusTextHistory() + => this.items.Add(new("")); + + public HistoryItem Back(string text, (PostId StatusId, string ScreenName)? inReplyTo) + { + if (!string.IsNullOrWhiteSpace(text)) + this.items[this.historyIndex] = new(text, inReplyTo); + + this.historyIndex -= 1; + if (this.historyIndex < 0) + this.historyIndex = 0; + + return this.items[this.historyIndex]; + } + + public HistoryItem Forward(string text, (PostId StatusId, string ScreenName)? inReplyTo) + { + if (!string.IsNullOrWhiteSpace(text)) + this.items[this.historyIndex] = new(text, inReplyTo); + + this.historyIndex += 1; + if (this.historyIndex > this.items.Count - 1) + this.historyIndex = this.items.Count - 1; + + return this.items[this.historyIndex]; + } + + public void SetLastItem(string text, (PostId StatusId, string ScreenName)? inReplyTo) + => this.items[this.items.Count - 1] = new(text, inReplyTo); + + public void AddLast() + { + this.items.Add(new("")); + this.historyIndex = this.items.Count - 1; + } + + public HistoryItem? Peek() + { + if (this.items.Count < 2) + return null; + + return this.items[this.items.Count - 2]; + } + } +} diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 7124676b2..7d924e375 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -140,10 +140,7 @@ public partial class TweenMain : OTBaseForm private readonly ThumbnailGenerator thumbGenerator; /// 発言履歴 - private readonly List history = new(); - - /// 発言履歴カレントインデックス - private int hisIdx; + private readonly StatusTextHistory history = new(); // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない) @@ -239,11 +236,6 @@ internal enum SEARCHTYPE PrevSearch, } - private readonly record struct StatusTextHistory( - string Status, - (PostId StatusId, string ScreenName)? InReplyTo = null - ); - private readonly HookGlobalHotkey hookGlobalHotkey; public TweenMain( @@ -347,8 +339,6 @@ ThumbnailGenerator thumbGenerator this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]"; - this.history.Add(new StatusTextHistory("")); - this.hisIdx = 0; this.inReplyTo = null; // 各種ダイアログ設定 @@ -1098,14 +1088,7 @@ private async void MyList_SelectedIndexChanged(object sender, EventArgs e) private void StatusTextHistoryBack() { - if (!string.IsNullOrWhiteSpace(this.StatusText.Text)) - this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); - - this.hisIdx -= 1; - if (this.hisIdx < 0) - this.hisIdx = 0; - - var historyItem = this.history[this.hisIdx]; + var historyItem = this.history.Back(this.StatusText.Text, this.inReplyTo); this.inReplyTo = historyItem.InReplyTo; this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; @@ -1113,14 +1096,7 @@ private void StatusTextHistoryBack() private void StatusTextHistoryForward() { - if (!string.IsNullOrWhiteSpace(this.StatusText.Text)) - this.history[this.hisIdx] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); - - this.hisIdx += 1; - if (this.hisIdx > this.history.Count - 1) - this.hisIdx = this.history.Count - 1; - - var historyItem = this.history[this.hisIdx]; + var historyItem = this.history.Forward(this.StatusText.Text, this.inReplyTo); this.inReplyTo = historyItem.InReplyTo; this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; @@ -1164,7 +1140,7 @@ private async void PostButton_Click(object sender, EventArgs e) return; } - this.history[this.history.Count - 1] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo); + this.history.SetLastItem(this.StatusText.Text, this.inReplyTo); if (this.settings.Common.Nicoms) { @@ -1227,8 +1203,7 @@ private async void PostButton_Click(object sender, EventArgs e) this.inReplyTo = null; this.StatusText.Text = ""; - this.history.Add(new StatusTextHistory("")); - this.hisIdx = this.history.Count - 1; + this.history.AddLast(); if (!this.settings.Common.FocusLockToStatusText) this.CurrentListView.Focus(); this.urlUndoBuffer = null; @@ -6903,8 +6878,8 @@ private void SetMainWindowTitle() ttl.Append("Ver:").Append(MyCommon.GetReadableVersion()); break; case MyCommon.DispTitleEnum.Post: - if (this.history != null && this.history.Count > 1) - ttl.Append(this.history[this.history.Count - 2].Status.Replace("\r\n", " ")); + if (this.history.Peek() is { } lastItem) + ttl.Append(lastItem.Status.Replace("\r\n", " ")); break; case MyCommon.DispTitleEnum.UnreadRepCount: ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText1, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount); From d8d484c6db8f4b626cda2647fa25331656aad2ae Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 Jan 2024 09:28:41 +0900 Subject: [PATCH 04/16] =?UTF-8?q?SetLastItem=E3=81=A8AddLast=E3=83=A1?= =?UTF-8?q?=E3=82=BD=E3=83=83=E3=83=89=E3=82=92=E7=B5=B1=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/Models/StatusTextHistoryTest.cs | 12 ++++-------- OpenTween/Models/StatusTextHistory.cs | 7 +++---- OpenTween/Tween.cs | 5 ++--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/OpenTween.Tests/Models/StatusTextHistoryTest.cs b/OpenTween.Tests/Models/StatusTextHistoryTest.cs index 922522c6a..c33fe441b 100644 --- a/OpenTween.Tests/Models/StatusTextHistoryTest.cs +++ b/OpenTween.Tests/Models/StatusTextHistoryTest.cs @@ -48,8 +48,7 @@ public void Back_NoItemsTest() public void Back_HasItemsTest() { var history = new StatusTextHistory(); - history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.AddLast(); + history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); Assert.Equal(2, history.Items.Count); @@ -72,8 +71,7 @@ public void Forward_NoItemsTest() public void Forward_HasItemsTest() { var history = new StatusTextHistory(); - history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.AddLast(); + history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); history.Forward("@hoge aaa 123", (new TwitterStatusId("111"), "hoge")); @@ -87,8 +85,7 @@ public void Forward_HasItemsTest() public void AddLast_Test() { var history = new StatusTextHistory(); - history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.AddLast(); + history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); Assert.Equal(2, history.Items.Count); Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); Assert.Equal(new("", null), history.Items[1]); @@ -106,8 +103,7 @@ public void Peek_EmptyTest() public void Peek_HasItemsTest() { var history = new StatusTextHistory(); - history.SetLastItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.AddLast(); + history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Peek()); } diff --git a/OpenTween/Models/StatusTextHistory.cs b/OpenTween/Models/StatusTextHistory.cs index ec1d8fe24..f84c66d7f 100644 --- a/OpenTween/Models/StatusTextHistory.cs +++ b/OpenTween/Models/StatusTextHistory.cs @@ -68,11 +68,10 @@ public HistoryItem Forward(string text, (PostId StatusId, string ScreenName)? in return this.items[this.historyIndex]; } - public void SetLastItem(string text, (PostId StatusId, string ScreenName)? inReplyTo) - => this.items[this.items.Count - 1] = new(text, inReplyTo); - - public void AddLast() + public void AddLast(string text, (PostId StatusId, string ScreenName)? inReplyTo) { + this.items[this.items.Count - 1] = new(text, inReplyTo); + this.items.Add(new("")); this.historyIndex = this.items.Count - 1; } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 7d924e375..51974e01a 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -1140,8 +1140,6 @@ private async void PostButton_Click(object sender, EventArgs e) return; } - this.history.SetLastItem(this.StatusText.Text, this.inReplyTo); - if (this.settings.Common.Nicoms) { this.StatusText.SelectionStart = this.StatusText.Text.Length; @@ -1201,9 +1199,10 @@ private async void PostButton_Click(object sender, EventArgs e) uploadService = this.ImageSelector.Model.GetService(serviceName); } + this.history.AddLast(this.StatusText.Text, this.inReplyTo); + this.inReplyTo = null; this.StatusText.Text = ""; - this.history.AddLast(); if (!this.settings.Common.FocusLockToStatusText) this.CurrentListView.Focus(); this.urlUndoBuffer = null; From ef3f32a8050a6bea2d208bb32e705dfbd5a615bd Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Mon, 8 Jan 2024 09:54:28 +0900 Subject: [PATCH 05/16] =?UTF-8?q?Forward/Back=E3=83=A1=E3=82=BD=E3=83=83?= =?UTF-8?q?=E3=83=89=E3=81=8B=E3=82=89=E7=8F=BE=E5=9C=A8=E3=81=AE=E5=B1=A5?= =?UTF-8?q?=E6=AD=B4=E3=82=92=E6=9B=B4=E6=96=B0=E3=81=99=E3=82=8B=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E3=82=92=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/StatusTextHistoryTest.cs | 30 ++++++++++++------- OpenTween/Models/StatusTextHistory.cs | 25 ++++++++-------- OpenTween/Tween.cs | 6 ++-- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/OpenTween.Tests/Models/StatusTextHistoryTest.cs b/OpenTween.Tests/Models/StatusTextHistoryTest.cs index c33fe441b..01e240088 100644 --- a/OpenTween.Tests/Models/StatusTextHistoryTest.cs +++ b/OpenTween.Tests/Models/StatusTextHistoryTest.cs @@ -35,25 +35,35 @@ public void Initialize_Test() } [Fact] - public void Back_NoItemsTest() + public void SetCurrentItem_Test() { var history = new StatusTextHistory(); - history.Back("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.SetCurrentItem("@hoge aaa", (new TwitterStatusId("111"), "hoge")); Assert.Single(history.Items); Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); Assert.Equal(0, history.HistoryIndex); } + [Fact] + public void Back_NoItemsTest() + { + var history = new StatusTextHistory(); + history.Back(); + Assert.Single(history.Items); + Assert.Equal(new("", null), history.Items[0]); + Assert.Equal(0, history.HistoryIndex); + } + [Fact] public void Back_HasItemsTest() { var history = new StatusTextHistory(); history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); + history.Back(); Assert.Equal(2, history.Items.Count); Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); - Assert.Equal(new("@foo bbb", (new TwitterStatusId("222"), "foo")), history.Items[1]); + Assert.Equal(new("", null), history.Items[1]); Assert.Equal(0, history.HistoryIndex); } @@ -61,9 +71,9 @@ public void Back_HasItemsTest() public void Forward_NoItemsTest() { var history = new StatusTextHistory(); - history.Forward("@hoge aaa", (new TwitterStatusId("111"), "hoge")); + history.Forward(); Assert.Single(history.Items); - Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(new("", null), history.Items[0]); Assert.Equal(0, history.HistoryIndex); } @@ -72,12 +82,12 @@ public void Forward_HasItemsTest() { var history = new StatusTextHistory(); history.AddLast("@hoge aaa", (new TwitterStatusId("111"), "hoge")); - history.Back("@foo bbb", (new TwitterStatusId("222"), "foo")); - history.Forward("@hoge aaa 123", (new TwitterStatusId("111"), "hoge")); + history.Back(); + history.Forward(); Assert.Equal(2, history.Items.Count); - Assert.Equal(new("@hoge aaa 123", (new TwitterStatusId("111"), "hoge")), history.Items[0]); - Assert.Equal(new("@foo bbb", (new TwitterStatusId("222"), "foo")), history.Items[1]); + Assert.Equal(new("@hoge aaa", (new TwitterStatusId("111"), "hoge")), history.Items[0]); + Assert.Equal(new("", null), history.Items[1]); Assert.Equal(1, history.HistoryIndex); } diff --git a/OpenTween/Models/StatusTextHistory.cs b/OpenTween/Models/StatusTextHistory.cs index f84c66d7f..91cca92a4 100644 --- a/OpenTween/Models/StatusTextHistory.cs +++ b/OpenTween/Models/StatusTextHistory.cs @@ -44,36 +44,35 @@ internal int HistoryIndex public StatusTextHistory() => this.items.Add(new("")); - public HistoryItem Back(string text, (PostId StatusId, string ScreenName)? inReplyTo) + public void SetCurrentItem(string text, (PostId StatusId, string ScreenName)? inReplyTo) { if (!string.IsNullOrWhiteSpace(text)) this.items[this.historyIndex] = new(text, inReplyTo); + } - this.historyIndex -= 1; - if (this.historyIndex < 0) - this.historyIndex = 0; + public HistoryItem Back() + { + if (this.historyIndex > 0) + this.historyIndex--; return this.items[this.historyIndex]; } - public HistoryItem Forward(string text, (PostId StatusId, string ScreenName)? inReplyTo) + public HistoryItem Forward() { - if (!string.IsNullOrWhiteSpace(text)) - this.items[this.historyIndex] = new(text, inReplyTo); - - this.historyIndex += 1; - if (this.historyIndex > this.items.Count - 1) - this.historyIndex = this.items.Count - 1; + if (this.historyIndex < this.items.Count - 1) + this.historyIndex++; return this.items[this.historyIndex]; } public void AddLast(string text, (PostId StatusId, string ScreenName)? inReplyTo) { - this.items[this.items.Count - 1] = new(text, inReplyTo); + this.historyIndex = this.items.Count - 1; + this.SetCurrentItem(text, inReplyTo); this.items.Add(new("")); - this.historyIndex = this.items.Count - 1; + this.historyIndex++; } public HistoryItem? Peek() diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 51974e01a..f2d9080c7 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -1088,7 +1088,8 @@ private async void MyList_SelectedIndexChanged(object sender, EventArgs e) private void StatusTextHistoryBack() { - var historyItem = this.history.Back(this.StatusText.Text, this.inReplyTo); + this.history.SetCurrentItem(this.StatusText.Text, this.inReplyTo); + var historyItem = this.history.Back(); this.inReplyTo = historyItem.InReplyTo; this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; @@ -1096,7 +1097,8 @@ private void StatusTextHistoryBack() private void StatusTextHistoryForward() { - var historyItem = this.history.Forward(this.StatusText.Text, this.inReplyTo); + this.history.SetCurrentItem(this.StatusText.Text, this.inReplyTo); + var historyItem = this.history.Forward(); this.inReplyTo = historyItem.InReplyTo; this.StatusText.Text = historyItem.Status; this.StatusText.SelectionStart = this.StatusText.Text.Length; From 2e5a033965984f40bae17850fe000a4b32027c78 Mon Sep 17 00:00:00 2001 From: Kimura Youichi Date: Thu, 11 Jan 2024 04:42:45 +0900 Subject: [PATCH 06/16] =?UTF-8?q?TweenMain.AddNewTab=E3=83=A1=E3=82=BD?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=81=AB=E5=AF=BE=E3=81=99=E3=82=8B=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenTween.Tests/ListElementTest.cs | 54 ++++++++++++++ OpenTween.Tests/TweenMainTest.cs | 115 ++++++++++++++++++++++++++++- OpenTween/ListElement.cs | 2 +- 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 OpenTween.Tests/ListElementTest.cs diff --git a/OpenTween.Tests/ListElementTest.cs b/OpenTween.Tests/ListElementTest.cs new file mode 100644 index 000000000..cba9f40c5 --- /dev/null +++ b/OpenTween.Tests/ListElementTest.cs @@ -0,0 +1,54 @@ +// 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 Xunit; + +namespace OpenTween +{ + public class ListElementTest + { + [Fact] + public void ToString_PublicTest() + { + var list = new ListElement + { + Id = 12345L, + Name = "tetete", + Username = "opentween", + IsPublic = true, + }; + Assert.Equal("@opentween/tetete [Public]", list.ToString()); + } + + [Fact] + public void ToString_ProtectedTest() + { + var list = new ListElement + { + Id = 12345L, + Name = "tetete", + Username = "opentween", + IsPublic = false, + }; + Assert.Equal("@opentween/tetete [Protected]", list.ToString()); + } + } +} diff --git a/OpenTween.Tests/TweenMainTest.cs b/OpenTween.Tests/TweenMainTest.cs index 8beba3b2c..2b6da7991 100644 --- a/OpenTween.Tests/TweenMainTest.cs +++ b/OpenTween.Tests/TweenMainTest.cs @@ -30,6 +30,7 @@ using OpenTween.Api.DataModel; using OpenTween.Connection; using OpenTween.Models; +using OpenTween.OpenTweenCustomControl; using OpenTween.Setting; using OpenTween.Thumbnail; using Xunit; @@ -40,7 +41,8 @@ namespace OpenTween public class TweenMainTest { private record TestContext( - SettingManager Settings + SettingManager Settings, + TabInformations TabInfo ); private void UsingTweenMain(Action func) @@ -54,7 +56,7 @@ private void UsingTweenMain(Action func) var thumbnailGenerator = new ThumbnailGenerator(new(autoupdate: false)); using var tweenMain = new TweenMain(settings, tabinfo, twitter, imageCache, iconAssets, thumbnailGenerator); - var context = new TestContext(settings); + var context = new TestContext(settings, tabinfo); func(tweenMain, context); } @@ -63,6 +65,115 @@ private void UsingTweenMain(Action func) public void Initialize_Test() => this.UsingTweenMain((_, _) => { }); + [WinFormsFact] + public void AddNewTab_FilterTabTest() + { + this.UsingTweenMain((tweenMain, context) => + { + Assert.Equal(4, tweenMain.ListTab.TabPages.Count); + + var tab = new FilterTabModel("hoge"); + context.TabInfo.AddTab(tab); + tweenMain.AddNewTab(tab, startup: false); + + Assert.Equal(5, tweenMain.ListTab.TabPages.Count); + + var tabPage = tweenMain.ListTab.TabPages[4]; + Assert.Equal("hoge", tabPage.Text); + Assert.Single(tabPage.Controls); + Assert.IsType(tabPage.Controls[0]); + }); + } + + [WinFormsFact] + public void AddNewTab_UserTimelineTabTest() + { + this.UsingTweenMain((tweenMain, context) => + { + Assert.Equal(4, tweenMain.ListTab.TabPages.Count); + + var tab = new UserTimelineTabModel("hoge", "twitterapi"); + context.TabInfo.AddTab(tab); + tweenMain.AddNewTab(tab, startup: false); + + Assert.Equal(5, tweenMain.ListTab.TabPages.Count); + + var tabPage = tweenMain.ListTab.TabPages[4]; + Assert.Equal("hoge", tabPage.Text); + Assert.Equal(2, tabPage.Controls.Count); + Assert.IsType(tabPage.Controls[0]); + + var label = Assert.IsType