OSDN Git Service

DeleteRetweetを使用したリツイートの取消に対応
[opentween/open-tween.git] / OpenTween / Tween.cs
index ecad417..e0033aa 100644 (file)
@@ -98,10 +98,10 @@ 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\"><!-- "
+            """<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%);} "
+            + """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;} "
@@ -167,7 +167,7 @@ namespace OpenTween
         // 発言投稿時の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();
@@ -219,8 +219,8 @@ namespace OpenTween
         private List<UrlUndo>? urlUndoBuffer = null;
 
         private readonly record struct ReplyChain(
-            long OriginalId,
-            long InReplyToId,
+            PostId OriginalId,
+            PostId InReplyToId,
             TabModel OriginalTab
         );
 
@@ -258,7 +258,7 @@ namespace OpenTween
 
         private readonly record struct StatusTextHistory(
             string Status,
-            (long StatusId, string ScreenName)? InReplyTo = null
+            (PostId StatusId, string ScreenName)? InReplyTo = null
         );
 
         private readonly HookGlobalHotkey hookGlobalHotkey;
@@ -522,7 +522,11 @@ namespace OpenTween
             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);
+            var account = this.settings.Common.SelectedAccount;
+            if (account != null)
+                this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+            else
+                this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
 
             this.initial = true;
 
@@ -558,7 +562,7 @@ namespace OpenTween
             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, "@");
@@ -1044,7 +1048,7 @@ namespace OpenTween
                             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
@@ -1240,7 +1244,7 @@ namespace OpenTween
             var status = new PostStatusParams();
 
             var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
-            if (this.GetRestStatusCount(statusTextCompat) >= 0)
+            if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AppToken.AuthType == APIAuthType.OAuth1)
             {
                 // auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
                 // 収まる場合はこれらのオプションを使用せずに投稿する
@@ -1414,7 +1418,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavAddAsync(long statusId, TabModel tab)
+        private async Task FavAddAsync(PostId statusId, TabModel tab)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -1436,7 +1440,7 @@ namespace OpenTween
             }
         }
 
-        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;
@@ -1456,9 +1460,10 @@ namespace OpenTween
 
                 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);
                     }
@@ -1470,7 +1475,7 @@ namespace OpenTween
 
                     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)
@@ -1534,7 +1539,7 @@ namespace OpenTween
             }
         }
 
-        private async Task FavRemoveAsync(IReadOnlyList<long> statusIds, TabModel tab)
+        private async Task FavRemoveAsync(IReadOnlyList<PostId> statusIds, TabModel tab)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -1556,7 +1561,7 @@ namespace OpenTween
             }
         }
 
-        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;
@@ -1564,7 +1569,7 @@ namespace OpenTween
             if (!CheckAccountValid())
                 throw new WebApiException("Auth error. Check your account");
 
-            var successIds = new List<long>();
+            var successIds = new List<PostId>();
 
             await Task.Run(async () =>
             {
@@ -1582,9 +1587,11 @@ namespace OpenTween
                     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);
                     }
@@ -1792,7 +1799,7 @@ namespace OpenTween
                 await this.RefreshTabAsync<HomeTabModel>();
         }
 
-        private async Task RetweetAsync(IReadOnlyList<long> statusIds)
+        private async Task RetweetAsync(IReadOnlyList<PostId> statusIds)
         {
             await this.workerSemaphore.WaitAsync();
 
@@ -1814,7 +1821,7 @@ namespace OpenTween
             }
         }
 
-        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;
@@ -2385,9 +2392,9 @@ namespace OpenTween
 
                     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
                         {
@@ -2395,8 +2402,7 @@ namespace OpenTween
                             {
                                 // 自分が RT したツイート (自分が RT した自分のツイートも含む)
                                 //   => RT を取り消し
-                                await this.tw.Api.StatusesDestroy(post.StatusId)
-                                    .IgnoreResponse();
+                                await this.tw.DeleteRetweet(post);
                             }
                             else
                             {
@@ -2406,15 +2412,13 @@ namespace OpenTween
                                     {
                                         // 他人に 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());
                                     }
                                 }
                             }
@@ -2581,7 +2585,12 @@ namespace OpenTween
                     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);
+                    var account = this.settings.Common.SelectedAccount;
+                    if (account != null)
+                        this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+                    else
+                        this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
+
                     this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
                     this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
 
@@ -4210,15 +4219,17 @@ namespace OpenTween
                 this.tweetDetailsView.ShowPostDetails(currentPost),
             };
 
-            this.SplitContainer3.Panel2Collapsed = true;
-
             if (this.settings.Common.PreviewEnable)
             {
                 var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource());
                 oldTokenSource?.Cancel();
 
                 var token = this.thumbnailTokenSource!.Token;
-                loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token));
+                loadTasks.Add(this.PrepareThumbnailControl(currentPost, token));
+            }
+            else
+            {
+                this.SplitContainer3.Panel2Collapsed = true;
             }
 
             async Task DelayedTasks()
@@ -4236,6 +4247,25 @@ namespace OpenTween
             _ = DelayedTasks();
         }
 
+        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)
             => await this.OpenApplicationWebsite();
 
@@ -4837,15 +4867,15 @@ namespace OpenTween
                     .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()),
             };
         }
 
@@ -5010,7 +5040,7 @@ namespace OpenTween
                 return;
 
             var selectedStatusId = tab.SelectedStatusId;
-            if (selectedStatusId == -1)
+            if (selectedStatusId == null)
                 return;
 
             int fIdx, toIdx, stp;
@@ -5188,7 +5218,7 @@ namespace OpenTween
             if (anchorStatusId == null)
                 return;
 
-            var idx = this.CurrentTab.IndexOf(anchorStatusId.Value);
+            var idx = this.CurrentTab.IndexOf(anchorStatusId);
             if (idx == -1)
                 return;
 
@@ -5305,11 +5335,15 @@ namespace OpenTween
             {
                 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;
@@ -5327,11 +5361,11 @@ namespace OpenTween
             {
                 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
@@ -5349,7 +5383,7 @@ namespace OpenTween
                 {
                     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;
 
@@ -5360,7 +5394,7 @@ namespace OpenTween
                 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;
                 }
 
@@ -5369,7 +5403,7 @@ namespace OpenTween
                 inReplyPost = inReplyToPosts.FirstOrDefault();
                 if (inReplyPost == null)
                 {
-                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+                    await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
                     return;
                 }
             }
@@ -5597,10 +5631,8 @@ namespace OpenTween
             }
         }
 
-        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))
@@ -5621,10 +5653,8 @@ namespace OpenTween
             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);
 
@@ -5831,7 +5861,7 @@ namespace OpenTween
 
             try
             {
-                var statusId = long.Parse(match.Groups["StatusId"].Value);
+                var statusId = new TwitterStatusId(match.Groups["StatusId"].Value);
                 await this.OpenRelatedTab(statusId);
             }
             catch (OverflowException)
@@ -5879,7 +5909,7 @@ namespace OpenTween
                                  "\"" + 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);
@@ -5896,7 +5926,7 @@ namespace OpenTween
                                  "\"" + 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);
@@ -7310,10 +7340,10 @@ namespace OpenTween
             {
                 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);
                 }
@@ -7321,12 +7351,12 @@ namespace OpenTween
                 {
                     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()));
                 }
             }
         }
@@ -7888,7 +7918,7 @@ namespace OpenTween
             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;
             }
@@ -8173,13 +8203,13 @@ namespace OpenTween
             // 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)
@@ -8682,66 +8712,20 @@ namespace OpenTween
 
         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;
-
-                        this.statuses.AddTab(tb);
-                        this.AddNewTab(tb, startup: false);
+                var restoredTab = this.statuses.UndoRemovedTab();
+                this.AddNewTab(restoredTab, startup: false);
 
-                        var tabIndex = this.statuses.Tabs.Count - 1;
-                        this.ListTab.SelectedIndex = tabIndex;
-                    }
-                }
-                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()
@@ -8990,14 +8974,7 @@ namespace OpenTween
 
         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;
@@ -9122,7 +9099,7 @@ namespace OpenTween
 
                 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)
@@ -9313,14 +9290,14 @@ namespace OpenTween
         /// </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)
                 {
@@ -9485,7 +9462,7 @@ namespace OpenTween
 
         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}"))
                 {
@@ -9496,7 +9473,7 @@ namespace OpenTween
                         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);
                     }
@@ -9523,11 +9500,11 @@ namespace OpenTween
                     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();
                     }
                 });
             }
@@ -9539,22 +9516,6 @@ namespace OpenTween
             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);