using System.Windows.Forms;
using OpenTween.Api;
using OpenTween.Api.DataModel;
+using OpenTween.Api.GraphQL;
using OpenTween.Api.TwitterV2;
using OpenTween.Connection;
using OpenTween.MediaUploadServices;
using OpenTween.Models;
using OpenTween.OpenTweenCustomControl;
using OpenTween.Setting;
+using OpenTween.SocialProtocol;
+using OpenTween.SocialProtocol.Twitter;
using OpenTween.Thumbnail;
namespace OpenTween
private readonly object syncObject = new(); // ロック用
- private const string DetailHtmlFormatHead =
- "<head><meta http-equiv=\"X-UA-Compatible\" content=\"IE=8\">"
- + "<style type=\"text/css\"><!-- "
- + "body, p, pre {margin: 0;} "
- + "body {font-family: \"%FONT_FAMILY%\", \"Segoe UI Emoji\", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} "
- + "pre {font-family: inherit;} "
- + "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
- + "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
- + ".quote-tweet {border: 1px solid #ccc; margin: 1em; padding: 0.5em;} "
- + ".quote-tweet.reply {border-color: rgb(%BG_REPLY_COLOR%);} "
- + ".quote-tweet-link {color: inherit !important; text-decoration: none;}"
- + "--></style>"
- + "</head>";
-
- private const string DetailHtmlFormatTemplateMono =
- $"<html>{DetailHtmlFormatHead}<body><pre>%CONTENT_HTML%</pre></body></html>";
-
- private const string DetailHtmlFormatTemplateNormal =
- $"<html>{DetailHtmlFormatHead}<body><p>%CONTENT_HTML%</p></body></html>";
-
- private string detailHtmlFormatPreparedTemplate = null!;
+ private readonly DetailsHtmlBuilder detailsHtmlBuilder = new();
private bool myStatusError = false;
private bool myStatusOnline = false;
// 設定ファイル
private readonly SettingManager settings;
- // twitter解析部
- private readonly Twitter tw;
+ // ユーザーアカウント
+ private readonly AccountCollection accounts;
+
+#pragma warning disable SA1300
+ private Twitter tw => ((TwitterAccount)this.accounts.Primary).Legacy; // AccountCollection への移行用
+#pragma warning restore SA1300
// Growl呼び出し部
private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName);
private readonly ThumbnailGenerator thumbGenerator;
/// <summary>発言履歴</summary>
- private readonly List<StatusTextHistory> history = new();
-
- /// <summary>発言履歴カレントインデックス</summary>
- private int hisIdx;
+ private readonly StatusTextHistory history = new();
// 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない)
/// <summary>リプライ先のステータスID・スクリーン名</summary>
- private (long StatusId, string ScreenName)? inReplyTo = null;
+ private (PostId StatusId, string ScreenName)? inReplyTo = null;
// 時速表示用
private readonly List<DateTimeUtc> postTimestamps = new();
private readonly DebounceTimer saveConfigDebouncer;
private readonly string recommendedStatusFooter;
- private bool urlMultibyteSplit = false;
+
+ internal bool SeparateUrlAndFullwidthCharacter { get; set; } = false;
+
private bool preventSmsCommand = true;
// URL短縮のUndo用
private List<UrlUndo>? urlUndoBuffer = null;
private readonly record struct ReplyChain(
- long OriginalId,
- long InReplyToId,
+ PostId OriginalId,
+ PostId InReplyToId,
TabModel OriginalTab
);
PrevSearch,
}
- private readonly record struct StatusTextHistory(
- string Status,
- (long StatusId, string ScreenName)? InReplyTo = null
- );
-
private readonly HookGlobalHotkey hookGlobalHotkey;
- private void TweenMain_Activated(object sender, EventArgs e)
- {
- // 画面がアクティブになったら、発言欄の背景色戻す
- if (this.StatusText.Focused)
- {
- this.StatusText_Enter(this.StatusText, System.EventArgs.Empty);
- }
- }
-
- private bool disposed = false;
-
- /// <summary>
- /// 使用中のリソースをすべてクリーンアップします。
- /// </summary>
- /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- if (this.disposed)
- return;
-
- if (disposing)
- {
- this.components?.Dispose();
-
- // 後始末
- this.SearchDialog.Dispose();
- this.urlDialog.Dispose();
- this.themeManager.Dispose();
- this.sfTab.Dispose();
-
- this.timelineScheduler.Dispose();
- this.workerCts.Cancel();
- this.thumbnailTokenSource?.Dispose();
-
- this.hookGlobalHotkey.Dispose();
- }
-
- // 終了時にRemoveHandlerしておかないとメモリリークする
- // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx
- Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged;
- Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged;
-
- this.disposed = true;
- }
-
- private void InitColumns(ListView list, bool startup)
- {
- this.InitColumnText();
-
- ColumnHeader[]? columns = null;
- try
- {
- if (this.Use2ColumnsMode)
- {
- columns = new[]
- {
- new ColumnHeader(), // アイコン
- new ColumnHeader(), // 本文
- };
-
- columns[0].Text = this.columnText[0];
- columns[1].Text = this.columnText[2];
-
- if (startup)
- {
- var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
-
- columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]);
- columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]);
- columns[0].DisplayIndex = 0;
- columns[1].DisplayIndex = 1;
- }
- else
- {
- var idx = 0;
- foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
- {
- columns[idx].Width = curListColumn.Width;
- columns[idx].DisplayIndex = curListColumn.DisplayIndex;
- idx++;
- }
- }
- }
- else
- {
- columns = new[]
- {
- new ColumnHeader(), // アイコン
- new ColumnHeader(), // ニックネーム
- new ColumnHeader(), // 本文
- new ColumnHeader(), // 日付
- new ColumnHeader(), // ユーザID
- new ColumnHeader(), // 未読
- new ColumnHeader(), // マーク&プロテクト
- new ColumnHeader(), // ソース
- };
-
- foreach (var i in Enumerable.Range(0, columns.Length))
- columns[i].Text = this.columnText[i];
-
- if (startup)
- {
- var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
-
- foreach (var (column, index) in columns.WithIndex())
- {
- column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]);
- column.DisplayIndex = this.settings.Local.ColumnsOrder[index];
- }
- }
- else
- {
- var idx = 0;
- foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
- {
- columns[idx].Width = curListColumn.Width;
- columns[idx].DisplayIndex = curListColumn.DisplayIndex;
- idx++;
- }
- }
- }
-
- list.Columns.AddRange(columns);
-
- columns = null;
- }
- finally
- {
- if (columns != null)
- {
- foreach (var column in columns)
- column?.Dispose();
- }
- }
- }
-
- private void InitColumnText()
- {
- this.columnText[0] = "";
- this.columnText[1] = Properties.Resources.AddNewTabText2;
- this.columnText[2] = Properties.Resources.AddNewTabText3;
- this.columnText[3] = Properties.Resources.AddNewTabText4_2;
- this.columnText[4] = Properties.Resources.AddNewTabText5;
- this.columnText[5] = "";
- this.columnText[6] = "";
- this.columnText[7] = "Source";
-
- this.columnOrgText[0] = "";
- this.columnOrgText[1] = Properties.Resources.AddNewTabText2;
- this.columnOrgText[2] = Properties.Resources.AddNewTabText3;
- this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2;
- this.columnOrgText[4] = Properties.Resources.AddNewTabText5;
- this.columnOrgText[5] = "";
- this.columnOrgText[6] = "";
- this.columnOrgText[7] = "Source";
-
- var c = this.statuses.SortMode switch
- {
- ComparerMode.Nickname => 1, // ニックネーム
- ComparerMode.Data => 2, // 本文
- ComparerMode.Id => 3, // 時刻=発言Id
- ComparerMode.Name => 4, // 名前
- ComparerMode.Source => 7, // Source
- _ => 0,
- };
-
- if (this.Use2ColumnsMode)
- {
- if (this.statuses.SortOrder == SortOrder.Descending)
- {
- // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
- this.columnText[2] = this.columnOrgText[2] + "▾";
- }
- else
- {
- // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
- this.columnText[2] = this.columnOrgText[2] + "▴";
- }
- }
- else
- {
- if (this.statuses.SortOrder == SortOrder.Descending)
- {
- // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
- this.columnText[c] = this.columnOrgText[c] + "▾";
- }
- else
- {
- // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
- this.columnText[c] = this.columnOrgText[c] + "▴";
- }
- }
- }
-
public TweenMain(
SettingManager settingManager,
TabInformations tabInfo,
- Twitter twitter,
+ AccountCollection accounts,
ImageCache imageCache,
IconAssetsManager iconAssets,
ThumbnailGenerator thumbGenerator
{
this.settings = settingManager;
this.statuses = tabInfo;
- this.tw = twitter;
+ this.accounts = accounts;
this.iconCache = imageCache;
this.iconAssets = iconAssets;
this.thumbGenerator = thumbGenerator;
this.InitializeShortcuts();
this.ignoreConfigSave = true;
- this.Visible = false;
this.TraceOutToolStripMenuItem.Checked = MyCommon.TraceFlag;
// 現在の DPI と設定保存時の DPI との比を取得する
var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
- // 認証関連
- this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId);
-
this.initial = true;
- this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
- this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
-
- // アクセストークンが有効であるか確認する
- // ここが Twitter API への最初のアクセスになるようにすること
- try
- {
- this.tw.VerifyCredentials();
- }
- catch (WebApiException ex)
- {
- MessageBox.Show(
- this,
- string.Format(Properties.Resources.StartupAuthError_Text, ex.Message),
- ApplicationSettings.ApplicationName,
- MessageBoxButtons.OK,
- MessageBoxIcon.Warning);
- }
-
// サムネイル関連の初期化
// プロキシ設定等の通信まわりの初期化が済んでから処理する
var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet;
imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet;
imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM;
- imgazyobizinet.AutoUpdate = true;
Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection;
// 画像投稿サービス
- this.ImageSelector.Initialize(this.tw, this.tw.Configuration, this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
+ this.ImageSelector.Model.SelectMediaService(this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
- this.tweetThumbnail1.Initialize(this.thumbGenerator);
+ this.tweetThumbnail1.Model.Initialize(this.thumbGenerator);
// ハッシュタグ/@id関連
this.AtIdSupl = new AtIdSupplement(this.settings.AtIdList.AtIdList, "@");
// フォント&文字色&背景色保持
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*", "") + "]";
- this.history.Add(new StatusTextHistory(""));
- this.hisIdx = 0;
this.inReplyTo = null;
// 各種ダイアログ設定
this.SetMainWindowTitle();
this.SetNotifyIconText();
- if (!this.settings.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized)
+ if (this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
{
- this.Visible = true;
+ this.Visible = false;
}
// タイマー設定
}));
this.RefreshTimelineScheduler();
- this.selectionDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true);
- this.saveConfigDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1));
+ this.selectionDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true);
+ this.saveConfigDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1));
+
+ // 更新中アイコンアニメーション間隔
+ this.TimerRefreshIcon.Interval = 200;
+ this.TimerRefreshIcon.Enabled = false;
+
+ this.ignoreConfigSave = false;
+ this.ApplyLayoutFromSettings();
+ }
+
+ private void TweenMain_Activated(object sender, EventArgs e)
+ {
+ // 画面がアクティブになったら、発言欄の背景色戻す
+ if (this.StatusText.Focused)
+ {
+ this.StatusText_Enter(this.StatusText, System.EventArgs.Empty);
+ }
+ }
+
+ private bool disposed = false;
+
+ /// <summary>
+ /// 使用中のリソースをすべてクリーンアップします。
+ /// </summary>
+ /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (this.disposed)
+ return;
+
+ if (disposing)
+ {
+ this.components?.Dispose();
+
+ // 後始末
+ this.SearchDialog.Dispose();
+ this.urlDialog.Dispose();
+ this.themeManager.Dispose();
+ this.sfTab.Dispose();
+
+ this.timelineScheduler.Dispose();
+ this.workerCts.Cancel();
+ this.thumbnailTokenSource?.Dispose();
+
+ this.hookGlobalHotkey.Dispose();
+ }
+
+ // 終了時にRemoveHandlerしておかないとメモリリークする
+ // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx
+ Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged;
+ Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged;
+ MyCommon.TwitterApiInfo.AccessLimitUpdated -= this.TwitterApiStatus_AccessLimitUpdated;
+
+ this.disposed = true;
+ }
+
+ private void InitColumns(ListView list, bool startup)
+ {
+ this.InitColumnText();
+
+ ColumnHeader[]? columns = null;
+ try
+ {
+ if (this.Use2ColumnsMode)
+ {
+ columns = new[]
+ {
+ new ColumnHeader(), // アイコン
+ new ColumnHeader(), // 本文
+ };
+
+ columns[0].Text = this.columnText[0];
+ columns[1].Text = this.columnText[2];
+
+ if (startup)
+ {
+ var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
+
+ columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]);
+ columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]);
+ columns[0].DisplayIndex = 0;
+ columns[1].DisplayIndex = 1;
+ }
+ else
+ {
+ var idx = 0;
+ foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
+ {
+ columns[idx].Width = curListColumn.Width;
+ columns[idx].DisplayIndex = curListColumn.DisplayIndex;
+ idx++;
+ }
+ }
+ }
+ else
+ {
+ columns = new[]
+ {
+ new ColumnHeader(), // アイコン
+ new ColumnHeader(), // ニックネーム
+ new ColumnHeader(), // 本文
+ new ColumnHeader(), // 日付
+ new ColumnHeader(), // ユーザID
+ new ColumnHeader(), // 未読
+ new ColumnHeader(), // マーク&プロテクト
+ new ColumnHeader(), // ソース
+ };
+
+ foreach (var i in Enumerable.Range(0, columns.Length))
+ columns[i].Text = this.columnText[i];
- // 更新中アイコンアニメーション間隔
- this.TimerRefreshIcon.Interval = 200;
- this.TimerRefreshIcon.Enabled = false;
+ if (startup)
+ {
+ var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
- this.ignoreConfigSave = false;
- this.TweenMain_Resize(this, EventArgs.Empty);
+ foreach (var (column, index) in columns.WithIndex())
+ {
+ column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]);
+ column.DisplayIndex = this.settings.Local.ColumnsOrder[index];
+ }
+ }
+ else
+ {
+ var idx = 0;
+ foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
+ {
+ columns[idx].Width = curListColumn.Width;
+ columns[idx].DisplayIndex = curListColumn.DisplayIndex;
+ idx++;
+ }
+ }
+ }
- if (this.settings.IsFirstRun)
+ list.Columns.AddRange(columns);
+
+ columns = null;
+ }
+ finally
{
- // 初回起動時だけ右下のメニューを目立たせる
- this.HashStripSplitButton.ShowDropDown();
+ if (columns != null)
+ {
+ foreach (var column in columns)
+ column?.Dispose();
+ }
}
}
- private void InitDetailHtmlFormat()
+ private void InitColumnText()
{
- var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
+ this.columnText[0] = "";
+ this.columnText[1] = Properties.Resources.AddNewTabText2;
+ this.columnText[2] = Properties.Resources.AddNewTabText3;
+ this.columnText[3] = Properties.Resources.AddNewTabText4_2;
+ this.columnText[4] = Properties.Resources.AddNewTabText5;
+ this.columnText[5] = "";
+ this.columnText[6] = "";
+ this.columnText[7] = "Source";
+
+ this.columnOrgText[0] = "";
+ this.columnOrgText[1] = Properties.Resources.AddNewTabText2;
+ this.columnOrgText[2] = Properties.Resources.AddNewTabText3;
+ this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2;
+ this.columnOrgText[4] = Properties.Resources.AddNewTabText5;
+ this.columnOrgText[5] = "";
+ this.columnOrgText[6] = "";
+ this.columnOrgText[7] = "Source";
- static string ColorToRGBString(Color color)
- => $"{color.R},{color.G},{color.B}";
+ var c = this.statuses.SortMode switch
+ {
+ ComparerMode.Nickname => 1, // ニックネーム
+ ComparerMode.Data => 2, // 本文
+ ComparerMode.Id => 3, // 時刻=発言Id
+ ComparerMode.Name => 4, // 名前
+ ComparerMode.Source => 7, // Source
+ _ => 0,
+ };
- 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));
+ if (this.Use2ColumnsMode)
+ {
+ if (this.statuses.SortOrder == SortOrder.Descending)
+ {
+ // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
+ this.columnText[2] = this.columnOrgText[2] + "▾";
+ }
+ else
+ {
+ // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
+ this.columnText[2] = this.columnOrgText[2] + "▴";
+ }
+ }
+ else
+ {
+ if (this.statuses.SortOrder == SortOrder.Descending)
+ {
+ // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
+ this.columnText[c] = this.columnOrgText[c] + "▾";
+ }
+ else
+ {
+ // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
+ this.columnText[c] = this.columnOrgText[c] + "▴";
+ }
+ }
}
private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
_ = this.saveConfigDebouncer.Call();
}
- private void RefreshTimeline()
+ internal void RefreshTimeline()
{
var curListView = this.CurrentListView;
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
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];
+ 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;
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];
+ 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;
return;
}
- this.history[this.history.Count - 1] = new StatusTextHistory(this.StatusText.Text, this.inReplyTo);
-
if (this.settings.Common.Nicoms)
{
this.StatusText.SelectionStart = this.StatusText.Text.Length;
var status = new PostStatusParams();
var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
- if (this.GetRestStatusCount(statusTextCompat) >= 0)
+ if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AuthType == APIAuthType.OAuth1)
{
// auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
// 収まる場合はこれらのオプションを使用せずに投稿する
if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
return;
- uploadService = this.ImageSelector.GetService(serviceName);
+ this.ImageSelector.EndSelection();
+ uploadService = this.ImageSelector.Model.GetService(serviceName);
}
+ this.history.AddLast(this.StatusText.Text, this.inReplyTo);
+
this.inReplyTo = null;
this.StatusText.Text = "";
- this.history.Add(new StatusTextHistory(""));
- this.hisIdx = this.history.Count - 1;
if (!this.settings.Common.FocusLockToStatusText)
this.CurrentListView.Focus();
this.urlUndoBuffer = null;
}
}
- private async Task FavAddAsync(long statusId, TabModel tab)
+ private async Task FavAddAsync(PostId statusId, TabModel tab)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, long statusId, TabModel tab)
+ private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, PostId statusId, TabModel tab)
{
if (ct.IsCancellationRequested)
return;
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);
}
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)
}
}
- private async Task FavRemoveAsync(IReadOnlyList<long> statusIds, TabModel tab)
+ private async Task FavRemoveAsync(IReadOnlyList<PostId> statusIds, TabModel tab)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds, TabModel tab)
+ private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds, TabModel tab)
{
if (ct.IsCancellationRequested)
return;
if (!CheckAccountValid())
throw new WebApiException("Auth error. Check your account");
- var successIds = new List<long>();
+ var successIds = new List<PostId>();
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);
}
await this.RefreshTabAsync<HomeTabModel>();
}
- private async Task RetweetAsync(IReadOnlyList<long> statusIds)
+ private async Task RetweetAsync(IReadOnlyList<PostId> statusIds)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds)
+ private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds)
{
if (ct.IsCancellationRequested)
return;
if (this.tw.Configuration.PhotoSizeLimit != 0)
{
- foreach (var service in this.ImageSelector.GetServices())
+ foreach (var (_, service) in this.ImageSelector.Model.MediaServices)
{
service.UpdateTwitterConfiguration(this.tw.Configuration);
}
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
{
{
// 自分が RT したツイート (自分が RT した自分のツイートも含む)
// => RT を取り消し
- await this.tw.Api.StatusesDestroy(post.StatusId)
- .IgnoreResponse();
+ await this.tw.DeleteRetweet(post);
}
else
{
{
// 他人に RT された自分のツイート
// => RT 元の自分のツイートを削除
- await this.tw.Api.StatusesDestroy(post.RetweetedId.Value)
- .IgnoreResponse();
+ await this.tw.DeleteTweet(post.RetweetedId.ToTwitterStatusId());
}
else
{
// 自分のツイート
// => ツイートを削除
- await this.tw.Api.StatusesDestroy(post.StatusId)
- .IgnoreResponse();
+ await this.tw.DeleteTweet(post.StatusId.ToTwitterStatusId());
}
}
}
{
this.settings.ApplySettings();
- if (MyCommon.IsNullOrEmpty(this.settings.Common.Token))
- this.tw.ClearAuthInfo();
-
- this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId);
- this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
- this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
-
- this.ImageSelector.Reset(this.tw, this.tw.Configuration);
+ this.accounts.LoadFromSettings(this.settings.Common);
+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
try
{
try
{
- this.InitDetailHtmlFormat();
+ this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager);
}
catch (Exception ex)
{
{
// アイコンサイズの再設定
if (this.listDrawer != null)
+ {
this.listDrawer.IconSize = iconSz;
+ this.listDrawer.UpdateItemHeight();
+ }
this.listCache?.PurgeCache();
}
attachmentUrl = null;
// attachment_url は media_id と同時に使用できない
- if (this.ImageSelector.Visible && this.ImageSelector.SelectedService is TwitterPhoto)
+ if (this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is TwitterPhoto)
return statusText;
var match = Twitter.AttachmentUrlRegex.Match(statusText);
return this.FormatStatusText(statusText);
}
+ internal string FormatStatusText(string statusText)
+ => this.FormatStatusText(statusText, Control.ModifierKeys);
+
/// <summary>
/// ツイート投稿前のフッター付与などの前処理を行います
/// </summary>
- private string FormatStatusText(string statusText)
+ internal string FormatStatusText(string statusText, Keys modifierKeys)
{
statusText = statusText.Replace("\r\n", "\n");
- if (this.urlMultibyteSplit)
+ if (this.SeparateUrlAndFullwidthCharacter)
{
// URLと全角文字の切り離し
statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& ");
bool disableFooter;
if (this.settings.Common.PostShiftEnter)
{
- disableFooter = MyCommon.IsKeyDown(Keys.Control);
+ disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control);
}
else
{
- if (this.StatusText.Multiline && !this.settings.Common.PostCtrlEnter)
- disableFooter = MyCommon.IsKeyDown(Keys.Control);
+ if (this.settings.Local.StatusMultiline && !this.settings.Common.PostCtrlEnter)
+ disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control);
else
- disableFooter = MyCommon.IsKeyDown(Keys.Shift);
+ disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Shift);
}
if (statusText.Contains("RT @"))
}
private IMediaUploadService? GetSelectedImageService()
- => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null;
+ => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null;
/// <summary>
/// 全てのタブの振り分けルールを反映し直します
if (dialog.ShowDialog(this) == DialogResult.Yes)
{
- await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri.OriginalString);
+ await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri);
}
else if (dialog.SkipButtonPressed)
{
this.DispSelectedPost();
}
- public string CreateDetailHtml(string orgdata)
- => this.detailHtmlFormatPreparedTemplate.Replace("%CONTENT_HTML%", orgdata);
-
private void DispSelectedPost()
=> this.DispSelectedPost(false);
if (!forceupdate && currentPost.Equals(oldDisplayPost))
return;
- var loadTasks = new List<Task>
- {
- this.tweetDetailsView.ShowPostDetails(currentPost),
- };
-
- this.SplitContainer3.Panel2Collapsed = true;
+ var loadTasks = new TaskCollection();
+ loadTasks.Add(() => this.tweetDetailsView.ShowPostDetails(currentPost));
if (this.settings.Common.PreviewEnable)
{
oldTokenSource?.Cancel();
var token = this.thumbnailTokenSource!.Token;
- loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token));
+ loadTasks.Add(() => this.PrepareThumbnailControl(currentPost, token));
}
-
- async Task DelayedTasks()
+ else
{
- try
- {
- await Task.WhenAll(loadTasks);
- }
- catch (OperationCanceledException)
- {
- }
+ this.SplitContainer3.Panel2Collapsed = true;
}
// サムネイルの読み込みを待たずに次に選択されたツイートを表示するため await しない
- _ = DelayedTasks();
+ _ = loadTasks
+ .IgnoreException(x => x is OperationCanceledException)
+ .RunAll();
+ }
+
+ private async Task PrepareThumbnailControl(PostClass post, CancellationToken token)
+ {
+ var prepareTask = this.tweetThumbnail1.Model.PrepareThumbnails(post, token);
+
+ var timeout = Task.Delay(100);
+ if ((await Task.WhenAny(prepareTask, timeout)) == timeout)
+ {
+ token.ThrowIfCancellationRequested();
+
+ // サムネイル情報の読み込みに時間が掛かっている場合は一旦サムネイル領域を非表示にする
+ this.SplitContainer3.Panel2Collapsed = true;
+ }
+
+ await prepareTask;
+ token.ThrowIfCancellationRequested();
+
+ this.SplitContainer3.Panel2Collapsed = !this.tweetThumbnail1.Model.ThumbnailAvailable;
}
private async void MatomeMenuItem_Click(object sender, EventArgs e)
.Do(() => this.CopyUserId()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Up)
- .Do(() => this.tweetThumbnail1.ScrollUp()),
+ .Do(() => this.tweetThumbnail1.Model.ScrollUp()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Down)
- .Do(() => this.tweetThumbnail1.ScrollDown()),
+ .Do(() => this.tweetThumbnail1.Model.ScrollDown()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
.FocusedOn(FocusedControl.ListTab)
.OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
- .Do(() => this.OpenThumbnailPicture(this.tweetThumbnail1.Thumbnail)),
+ .Do(() => this.tweetThumbnail1.OpenImageInBrowser()),
};
}
return;
var selectedStatusId = tab.SelectedStatusId;
- if (selectedStatusId == -1)
+ if (selectedStatusId == null)
return;
int fIdx, toIdx, stp;
if (anchorStatusId == null)
return;
- var idx = this.CurrentTab.IndexOf(anchorStatusId.Value);
+ var idx = this.CurrentTab.IndexOf(anchorStatusId);
if (idx == -1)
return;
{
try
{
- var post = await this.tw.GetStatusApi(false, currentPost.StatusId);
+ var post = await this.tw.GetStatusApi(false, currentPost.StatusId.ToTwitterStatusId());
- 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;
{
this.replyChains = new Stack<ReplyChain>();
}
- 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
{
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;
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;
}
inReplyPost = inReplyToPosts.FirstOrDefault();
if (inReplyPost == null)
{
- await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+ await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
return;
}
}
}
}
- 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))
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);
this.ModifySettingCommon = false;
lock (this.syncObject)
{
- this.settings.Common.UserName = this.tw.Username;
- this.settings.Common.UserId = this.tw.UserId;
- this.settings.Common.Token = this.tw.AccessToken;
- this.settings.Common.TokenSecret = this.tw.AccessTokenSecret;
this.settings.Common.SortOrder = (int)this.statuses.SortOrder;
this.settings.Common.SortColumn = this.statuses.SortMode switch
{
this.settings.Common.HashIsHead = this.HashMgr.IsHead;
this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent;
this.settings.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
- this.settings.Common.UseImageService = this.ImageSelector.ServiceIndex;
- this.settings.Common.UseImageServiceName = this.ImageSelector.ServiceName;
+ this.settings.Common.UseImageService = this.ImageSelector.Model.SelectedMediaServiceIndex;
+ this.settings.Common.UseImageServiceName = this.ImageSelector.Model.SelectedMediaServiceName;
this.settings.SaveCommon();
}
break;
case UserTimelineTabModel userTab:
tabSetting.User = userTab.ScreenName;
+ tabSetting.UserId = userTab.UserId;
break;
case PublicSearchTabModel searchTab:
tabSetting.SearchWords = searchTab.SearchWords;
try
{
- var statusId = long.Parse(match.Groups["StatusId"].Value);
+ var statusId = new TwitterStatusId(match.Groups["StatusId"].Value);
await this.OpenRelatedTab(statusId);
}
catch (OverflowException)
"\"" + 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);
"\"" + 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);
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);
if (endpointName == null)
{
+ var authByCookie = this.tw.Api.AuthType == APIAuthType.TwitterComCookie;
+
// 表示中のタブに応じて更新
endpointName = tabType switch
{
- MyCommon.TabUsageType.Home => GetTimelineRequest.EndpointName,
+ MyCommon.TabUsageType.Home => "/statuses/home_timeline",
MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
MyCommon.TabUsageType.Favorites => "/favorites/list",
MyCommon.TabUsageType.DirectMessage => "/direct_messages/events/list",
- MyCommon.TabUsageType.UserTimeline => "/statuses/user_timeline",
- MyCommon.TabUsageType.Lists => "/lists/statuses",
- MyCommon.TabUsageType.PublicSearch => "/search/tweets",
+ MyCommon.TabUsageType.UserTimeline =>
+ authByCookie ? UserTweetsAndRepliesRequest.EndpointName : "/statuses/user_timeline",
+ MyCommon.TabUsageType.Lists =>
+ authByCookie ? ListLatestTweetsTimelineRequest.EndpointName : "/lists/statuses",
+ MyCommon.TabUsageType.PublicSearch =>
+ authByCookie ? SearchTimelineRequest.EndpointName : "/search/tweets",
MyCommon.TabUsageType.Related => "/statuses/show/:id",
_ => null,
};
}
else
{
- // 表示中のタブに関連する endpoint であれば更新
- bool update;
- if (endpointName == GetTimelineRequest.EndpointName)
- {
- update = tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined;
- }
- else
- {
- update = endpointName switch
- {
- "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
- "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
- "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
- "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
- "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
- "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
- "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
- _ => false,
- };
- }
-
- if (update)
- {
- this.toolStripApiGauge.ApiEndpoint = endpointName;
- }
+ var currentEndpointName = this.toolStripApiGauge.ApiEndpoint;
+ this.toolStripApiGauge.ApiEndpoint = currentEndpointName;
}
}
{
this.Visible = false;
}
- if (this.initialLayout && this.settings.Local != null && this.WindowState == FormWindowState.Normal && this.Visible)
+ if (this.WindowState != FormWindowState.Minimized)
{
- // 現在の DPI と設定保存時の DPI との比を取得する
- var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
+ this.formWindowState = this.WindowState;
+ }
+ }
- this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize);
+ private void ApplyLayoutFromSettings()
+ {
+ // 現在の DPI と設定保存時の DPI との比を取得する
+ var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
- // Splitterの位置設定
- var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance);
- if (splitterDistance > this.SplitContainer1.Panel1MinSize &&
- splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth)
- {
- this.SplitContainer1.SplitterDistance = splitterDistance;
- }
+ this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize);
- // 発言欄複数行
- this.StatusText.Multiline = this.settings.Local.StatusMultiline;
- if (this.StatusText.Multiline)
- {
- var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight);
- var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
- if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth)
- {
- this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
- }
- this.StatusText.Height = statusTextHeight;
- }
- else
+ // Splitterの位置設定
+ var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance);
+ if (splitterDistance > this.SplitContainer1.Panel1MinSize &&
+ splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth)
+ {
+ this.SplitContainer1.SplitterDistance = splitterDistance;
+ }
+
+ // 発言欄複数行
+ this.StatusText.Multiline = this.settings.Local.StatusMultiline;
+ if (this.StatusText.Multiline)
+ {
+ var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight);
+ var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
+ if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth)
{
- if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0)
- {
- this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
- }
+ this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
}
-
- var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
- if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth)
+ this.StatusText.Height = statusTextHeight;
+ }
+ else
+ {
+ if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0)
{
- this.SplitContainer3.SplitterDistance = previewDistance;
+ this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
}
-
- // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない
- this.SplitContainer3.Panel2Collapsed = true;
-
- this.initialLayout = false;
}
- if (this.WindowState != FormWindowState.Minimized)
+
+ var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
+ if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth)
{
- this.formWindowState = this.WindowState;
+ this.SplitContainer3.SplitterDistance = previewDistance;
}
+
+ // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない
+ this.SplitContainer3.Panel2Collapsed = true;
+ this.initialLayout = false;
}
private void PlaySoundMenuItem_CheckedChanged(object sender, EventArgs e)
{
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);
}
{
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()));
}
}
}
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;
}
(this.listCache, var oldCache) = (newCache, this.listCache);
oldCache?.Dispose();
- var newDrawer = new TimelineListViewDrawer(listView, tab, this.listCache, this.iconCache, this.themeManager)
- {
- IconSize = this.settings.Common.IconSize,
- };
+ var newDrawer = new TimelineListViewDrawer(listView, tab, this.listCache, this.iconCache, this.themeManager);
(this.listDrawer, var oldDrawer) = (newDrawer, this.listDrawer);
oldDrawer?.Dispose();
+
+ newDrawer.IconSize = this.settings.Common.IconSize;
+ newDrawer.UpdateItemHeight();
}
private void ListTab_Selecting(object sender, TabControlCancelEventArgs e)
private async void TweenMain_Shown(object sender, EventArgs e)
{
this.NotifyIcon1.Visible = true;
+ this.StartTimers();
+
+ if (this.settings.IsFirstRun)
+ {
+ // 初回起動時だけ右下のメニューを目立たせる
+ this.HashStripSplitButton.ShowDropDown();
+ }
if (this.IsNetworkAvailable())
{
- var loadTasks = new List<Task>
- {
- this.RefreshMuteUserIdsAsync(),
- this.RefreshBlockIdsAsync(),
- this.RefreshNoRetweetIdsAsync(),
- this.RefreshTwitterConfigurationAsync(),
- this.RefreshTabAsync<HomeTabModel>(),
- this.RefreshTabAsync<MentionsTabModel>(),
- this.RefreshTabAsync<DirectMessagesTabModel>(),
- this.RefreshTabAsync<PublicSearchTabModel>(),
- this.RefreshTabAsync<UserTimelineTabModel>(),
- this.RefreshTabAsync<ListTimelineTabModel>(),
- };
+ var loadTasks = new TaskCollection();
+
+ loadTasks.Add(new[]
+ {
+ this.RefreshMuteUserIdsAsync,
+ this.RefreshBlockIdsAsync,
+ this.RefreshNoRetweetIdsAsync,
+ this.RefreshTwitterConfigurationAsync,
+ this.RefreshTabAsync<HomeTabModel>,
+ this.RefreshTabAsync<MentionsTabModel>,
+ this.RefreshTabAsync<DirectMessagesTabModel>,
+ this.RefreshTabAsync<PublicSearchTabModel>,
+ this.RefreshTabAsync<UserTimelineTabModel>,
+ this.RefreshTabAsync<ListTimelineTabModel>,
+ });
if (this.settings.Common.StartupFollowers)
- loadTasks.Add(this.RefreshFollowerIdsAsync());
+ loadTasks.Add(this.RefreshFollowerIdsAsync);
if (this.settings.Common.GetFav)
- loadTasks.Add(this.RefreshTabAsync<FavoritesTabModel>());
+ loadTasks.Add(this.RefreshTabAsync<FavoritesTabModel>);
- var allTasks = Task.WhenAll(loadTasks);
+ var allTasks = loadTasks.RunAll();
var i = 0;
while (true)
}
// 取得失敗の場合は再試行する
- var reloadTasks = new List<Task>();
+ var reloadTasks = new TaskCollection();
if (!this.tw.GetFollowersSuccess && this.settings.Common.StartupFollowers)
- reloadTasks.Add(this.RefreshFollowerIdsAsync());
+ reloadTasks.Add(() => this.RefreshFollowerIdsAsync());
if (!this.tw.GetNoRetweetSuccess)
- reloadTasks.Add(this.RefreshNoRetweetIdsAsync());
+ reloadTasks.Add(() => this.RefreshNoRetweetIdsAsync());
if (this.tw.Configuration.PhotoSizeLimit == 0)
- reloadTasks.Add(this.RefreshTwitterConfigurationAsync());
+ reloadTasks.Add(() => this.RefreshTwitterConfigurationAsync());
- await Task.WhenAll(reloadTasks);
+ await reloadTasks.RunAll();
}
this.initial = false;
+ }
- this.timelineScheduler.Enabled = true;
+ private void StartTimers()
+ {
+ if (!this.StopRefreshAllMenuItem.Checked)
+ this.timelineScheduler.Enabled = true;
+
+ this.selectionDebouncer.Enabled = true;
+ this.saveConfigDebouncer.Enabled = true;
+ this.thumbGenerator.ImgAzyobuziNet.AutoUpdate = true;
}
private async Task DoGetFollowersMenu()
{
if (!this.ExistCurrentPost) return;
this.doFavRetweetFlags = true;
- var retweetTask = this.DoReTweetOfficial(true);
+
+ var tasks = new TaskCollection();
+ tasks.Add(() => this.DoReTweetOfficial(true));
+
if (this.doFavRetweetFlags)
{
this.doFavRetweetFlags = false;
- var favoriteTask = this.FavoriteChange(true, false);
-
- await Task.WhenAll(retweetTask, favoriteTask);
- }
- else
- {
- await retweetTask;
+ tasks.Add(() => this.FavoriteChange(true, false));
}
+
+ await tasks.RunAll();
}
private async Task FavoritesRetweetUnofficial()
// TweetFormatterクラスによって整形された状態のHTMLを元のツイートに復元します
// 通常の URL
- statusHtml = Regex.Replace(statusHtml, "<a href=\"(?<href>.+?)\" title=\"(?<title>.+?)\">(?<text>.+?)</a>", "${title}");
+ statusHtml = Regex.Replace(statusHtml, """<a href="(?<href>.+?)" title="(?<title>.+?)">(?<text>.+?)</a>""", "${title}");
// メンション
- statusHtml = Regex.Replace(statusHtml, "<a class=\"mention\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<a class="mention" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
// ハッシュタグ
- statusHtml = Regex.Replace(statusHtml, "<a class=\"hashtag\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<a class="hashtag" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
// 絵文字
- statusHtml = Regex.Replace(statusHtml, "<img class=\"emoji\" src=\".+?\" alt=\"(?<text>.+?)\" />", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<img class="emoji" src=".+?" alt="(?<text>.+?)" />""", "${text}");
// <br> 除去
if (multiline)
private void MenuItemHelp_DropDownOpening(object sender, EventArgs e)
{
- if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock, Keys.Control, Keys.Shift))
+ if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock | Keys.Control | Keys.Shift))
this.DebugModeToolStripMenuItem.Visible = true;
else
this.DebugModeToolStripMenuItem.Visible = false;
}
private void UrlMultibyteSplitMenuItem_CheckedChanged(object sender, EventArgs e)
- => this.urlMultibyteSplit = ((ToolStripMenuItem)sender).Checked;
+ => this.SeparateUrlAndFullwidthCharacter = ((ToolStripMenuItem)sender).Checked;
private void PreventSmsCommandMenuItem_CheckedChanged(object sender, EventArgs e)
=> this.preventSmsCommand = ((ToolStripMenuItem)sender).Checked;
private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e)
{
- this.UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit;
+ this.UrlMultibyteSplitMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter;
this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand;
this.UrlAutoShortenMenuItem.Checked = this.settings.Common.UrlConvertAuto;
this.IdeographicSpaceToSpaceMenuItem.Checked = this.settings.Common.WideSpaceConvert;
private void ContextMenuPostMode_Opening(object sender, CancelEventArgs e)
{
- this.UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit;
+ this.UrlMultibyteSplitPullDownMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter;
this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand;
this.UrlAutoShortenPullDownMenuItem.Checked = this.settings.Common.UrlConvertAuto;
this.IdeographicSpaceToSpacePullDownMenuItem.Checked = this.settings.Common.WideSpaceConvert;
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 (tb.TabType == MyCommon.TabUsageType.Related)
- {
- var relatedTab = this.statuses.GetTabByType(MyCommon.TabUsageType.Related);
- if (relatedTab != null)
- {
- // 関連発言なら既存のタブを置き換える
- tb.TabName = relatedTab.TabName;
- this.ClearTab(tb.TabName, false);
-
- this.statuses.ReplaceTab(tb);
-
- var tabIndex = this.statuses.Tabs.IndexOf(tb);
- this.ListTab.SelectedIndex = tabIndex;
- }
- else
- {
- const string TabName = "Related Tweets";
- var renamed = TabName;
- for (var i = 2; i <= 100; i++)
- {
- if (!this.statuses.ContainsTab(renamed))
- break;
- renamed = TabName + i;
- }
- tb.TabName = renamed;
+ var restoredTab = this.statuses.UndoRemovedTab();
+ this.AddNewTab(restoredTab, startup: false);
- this.statuses.AddTab(tb);
- this.AddNewTab(tb, startup: false);
-
- var tabIndex = this.statuses.Tabs.Count - 1;
- this.ListTab.SelectedIndex = tabIndex;
- }
- }
- else
- {
- var renamed = tb.TabName;
- for (var i = 1; i < int.MaxValue; i++)
- {
- if (!this.statuses.ContainsTab(renamed))
- break;
- renamed = tb.TabName + "(" + i + ")";
- }
- tb.TabName = renamed;
-
- this.statuses.AddTab(tb);
- this.AddNewTab(tb, startup: false);
+ var tabIndex = this.statuses.Tabs.Count - 1;
+ this.ListTab.SelectedIndex = tabIndex;
- 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()
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;
try
{
- var task = this.tw.Api.UsersShow(id);
+ var task = this.tw.GetUserInfo(id);
user = await dialog.WaitForAsync(this, task);
}
catch (WebApiException ex)
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);
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)
private void SelectMedia_DragEnter(DragEventArgs e)
{
- if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
+ if (this.ImageSelector.Model.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
{
e.Effect = DragDropEffects.Copy;
return;
{
this.Activate();
this.BringToFront();
- this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false));
+
+ var filePathArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromFilePath(filePathArray);
this.StatusText.Focus();
}
}
else if (Clipboard.ContainsImage())
{
- // 画像があるので投稿処理を行う
- if (MessageBox.Show(Properties.Resources.PostPictureConfirm3,
- Properties.Resources.PostPictureWarn4,
- MessageBoxButtons.OKCancel,
- MessageBoxIcon.Question,
- MessageBoxDefaultButton.Button2)
- == DialogResult.OK)
- {
- // clipboardから画像を取得
- using var image = Clipboard.GetImage();
- this.ImageSelector.BeginSelection(image);
- }
+ // clipboardから画像を取得
+ using var image = Clipboard.GetImage();
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromImage(image);
}
else if (Clipboard.ContainsFileDropList())
{
var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
- this.ImageSelector.BeginSelection(files);
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromFilePath(files);
}
}
catch (ExternalException ex)
/// </summary>
/// <param name="statusId">表示するツイートのID</param>
/// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
- 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)
{
private async Task OpenUserAppointUrl()
{
- if (this.settings.Common.UserAppointUrl != null)
+ if (!MyCommon.IsNullOrEmpty(this.settings.Common.UserAppointUrl))
{
if (this.settings.Common.UserAppointUrl.Contains("{ID}") || this.settings.Common.UserAppointUrl.Contains("{STATUS}"))
{
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);
}
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();
}
});
}
this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text);
}
- private void TweetThumbnail_ThumbnailLoading(object sender, EventArgs e)
- => this.SplitContainer3.Panel2Collapsed = false;
-
- private async void TweetThumbnail_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e)
- => await this.OpenThumbnailPicture(e.Thumbnail);
-
- private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e)
- => await MyCommon.OpenInBrowserAsync(this, e.ImageUrl);
-
- private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail)
- {
- var url = thumbnail.FullSizeImageUrl ?? thumbnail.MediaPageUrl;
-
- await MyCommon.OpenInBrowserAsync(this, url);
- }
-
private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
=> await MyCommon.OpenInBrowserAsync(this, Twitter.ServiceAvailabilityStatusUrl);