OSDN Git Service

MemoryImage.Clone() で内部の byte[] をコピーせずに再利用する
[opentween/open-tween.git] / OpenTween / TweetDetailsView.cs
index 9909d82..8a3e0d3 100644 (file)
@@ -24,6 +24,8 @@
 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
 // Boston, MA 02110-1301, USA.
 
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -45,16 +47,16 @@ namespace OpenTween
 {
     public partial class TweetDetailsView : UserControl
     {
-        public TweenMain Owner { get; set; }
+        public TweenMain Owner { get; set; } = null!;
 
         /// <summary>プロフィール画像のキャッシュ</summary>
-        public ImageCache IconCache { get; set; }
+        public ImageCache IconCache { get; set; } = null!;
 
         /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
         public bool DumpPostClass { get; set; }
 
         /// <summary>現在表示中の発言</summary>
-        public PostClass CurrentPost { get; private set; }
+        public PostClass? CurrentPost { get; private set; }
 
         [DefaultValue(false)]
         public new bool TabStop
@@ -64,10 +66,10 @@ namespace OpenTween
         }
 
         /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
-        public event EventHandler<TweetDetailsViewStatusChengedEventArgs> StatusChanged;
+        public event EventHandler<TweetDetailsViewStatusChengedEventArgs>? StatusChanged;
 
         /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
-        private string _postBrowserStatusText = "";
+        private string postBrowserStatusText = "";
 
         public TweetDetailsView()
         {
@@ -75,15 +77,19 @@ namespace OpenTween
 
             this.TabStop = false;
 
-            //発言詳細部の初期化
-            NameLabel.Text = "";
-            DateTimeLabel.Text = "";
-            SourceLinkLabel.Text = "";
+            // 発言詳細部の初期化
+            this.AuthorNameLinkLabel.Text = "";
+            this.RetweetedByLinkLabel.Text = "";
+            this.DateTimeLabel.Text = "";
+            this.SourceLinkLabel.Text = "";
 
-            new InternetSecurityManager(PostBrowser);
+            new InternetSecurityManager(this.PostBrowser);
             this.PostBrowser.AllowWebBrowserDrop = false;  // COMException を回避するため、ActiveX の初期化が終わってから設定する
         }
 
+        public void ClearPostBrowser()
+            => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml("");
+
         public async Task ShowPostDetails(PostClass post)
         {
             this.CurrentPost = post;
@@ -92,8 +98,8 @@ namespace OpenTween
 
             using (ControlTransaction.Update(this.TableLayoutPanel1))
             {
-                SourceLinkLabel.Text = post.Source;
-                SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
+                this.SourceLinkLabel.Text = post.Source;
+                this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
 
                 string nameText;
                 if (post.IsDm)
@@ -108,10 +114,18 @@ namespace OpenTween
                     nameText = "";
                 }
                 nameText += post.ScreenName + "/" + post.Nickname;
-                if (post.RetweetedId != null)
-                    nameText += " (RT:" + post.RetweetedBy + ")";
+                this.AuthorNameLinkLabel.Text = nameText;
 
-                NameLabel.Text = nameText;
+                if (post.RetweetedId != null)
+                {
+                    this.RetweetedByLinkLabel.Visible = true;
+                    this.RetweetedByLinkLabel.Text = $"(RT:{post.RetweetedBy})";
+                }
+                else
+                {
+                    this.RetweetedByLinkLabel.Visible = false;
+                    this.RetweetedByLinkLabel.Text = "";
+                }
 
                 var nameForeColor = SystemColors.ControlText;
                 if (post.IsOwl && (SettingManager.Common.OneWayLove || post.IsDm))
@@ -120,22 +134,25 @@ namespace OpenTween
                     nameForeColor = SettingManager.Local.ColorRetweet;
                 if (post.IsFav)
                     nameForeColor = SettingManager.Local.ColorFav;
-                NameLabel.ForeColor = nameForeColor;
+
+                this.AuthorNameLinkLabel.LinkColor = nameForeColor;
+                this.AuthorNameLinkLabel.ActiveLinkColor = nameForeColor;
+                this.RetweetedByLinkLabel.LinkColor = nameForeColor;
+                this.RetweetedByLinkLabel.ActiveLinkColor = nameForeColor;
 
                 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
 
-                DateTimeLabel.Text = post.CreatedAt.ToString();
+                this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
             }
 
             if (this.DumpPostClass)
             {
-                StringBuilder sb = new StringBuilder(512);
+                var sb = new StringBuilder(512);
 
                 sb.Append("-----Start PostClass Dump<br>");
                 sb.AppendFormat("TextFromApi           : {0}<br>", post.TextFromApi);
                 sb.AppendFormat("(PlainText)    : <xmp>{0}</xmp><br>", post.TextFromApi);
                 sb.AppendFormat("StatusId             : {0}<br>", post.StatusId);
-                //sb.AppendFormat("ImageIndex     : {0}<br>", post.ImageIndex);
                 sb.AppendFormat("ImageUrl       : {0}<br>", post.ImageUrl);
                 sb.AppendFormat("InReplyToStatusId    : {0}<br>", post.InReplyToStatusId);
                 sb.AppendFormat("InReplyToUser  : {0}<br>", post.InReplyToUser);
@@ -148,7 +165,7 @@ namespace OpenTween
                 sb.AppendFormat("IsRead         : {0}<br>", post.IsRead);
                 sb.AppendFormat("IsReply        : {0}<br>", post.IsReply);
 
-                foreach (string nm in post.ReplyToList)
+                foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
                 {
                     sb.AppendFormat("ReplyToList    : {0}<br>", nm);
                 }
@@ -157,7 +174,7 @@ namespace OpenTween
                 sb.AppendFormat("NickName       : {0}<br>", post.Nickname);
                 sb.AppendFormat("Text   : {0}<br>", post.Text);
                 sb.AppendFormat("(PlainText)    : <xmp>{0}</xmp><br>", post.Text);
-                sb.AppendFormat("CreatedAt          : {0}<br>", post.CreatedAt);
+                sb.AppendFormat("CreatedAt          : {0}<br>", post.CreatedAt.ToLocalTimeString());
                 sb.AppendFormat("Source         : {0}<br>", post.Source);
                 sb.AppendFormat("UserId            : {0}<br>", post.UserId);
                 sb.AppendFormat("FilterHit      : {0}<br>", post.FilterHit);
@@ -167,7 +184,7 @@ namespace OpenTween
                 sb.AppendFormat("Media.Count    : {0}<br>", post.Media.Count);
                 if (post.Media.Count > 0)
                 {
-                    for (int i = 0; i < post.Media.Count; i++)
+                    for (var i = 0; i < post.Media.Count; i++)
                     {
                         var info = post.Media[i];
                         sb.AppendFormat("Media[{0}].Url         : {1}<br>", i, info.Url);
@@ -176,14 +193,14 @@ namespace OpenTween
                 }
                 sb.Append("-----End PostClass Dump<br>");
 
-                PostBrowser.DocumentText = this.Owner.createDetailHtml(sb.ToString());
+                this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString());
                 return;
             }
 
             using (ControlTransaction.Update(this.PostBrowser))
             {
                 this.PostBrowser.DocumentText =
-                    this.Owner.createDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
+                    this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
 
                 this.PostBrowser.Document.Window.ScrollTo(0, 0);
             }
@@ -195,7 +212,7 @@ namespace OpenTween
 
         public void ScrollDownPostBrowser(bool forward)
         {
-            var doc = PostBrowser.Document;
+            var doc = this.PostBrowser.Document;
             if (doc == null) return;
 
             var tags = doc.GetElementsByTagName("html");
@@ -210,16 +227,16 @@ namespace OpenTween
 
         public void PageDownPostBrowser(bool forward)
         {
-            var doc = PostBrowser.Document;
+            var doc = this.PostBrowser.Document;
             if (doc == null) return;
 
             var tags = doc.GetElementsByTagName("html");
             if (tags.Count > 0)
             {
                 if (forward)
-                    tags[0].ScrollTop += PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
+                    tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
                 else
-                    tags[0].ScrollTop -= PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
+                    tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
             }
         }
 
@@ -230,9 +247,9 @@ namespace OpenTween
                 .ToArray();
         }
 
-        private async Task SetUserPictureAsync(string imageUrl, bool force = false)
+        private async Task SetUserPictureAsync(string normalImageUrl, bool force = false)
         {
-            if (string.IsNullOrEmpty(imageUrl))
+            if (MyCommon.IsNullOrEmpty(normalImageUrl))
                 return;
 
             if (this.IconCache == null)
@@ -240,14 +257,31 @@ namespace OpenTween
 
             this.ClearUserPicture();
 
-            await this.UserPicture.SetImageFromTask(async () =>
+            var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width);
+            var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize);
+            if (cachedImage != null)
             {
-                var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
-                    .ConfigureAwait(false);
+                // 既にキャッシュされていればそれを表示して終了
+                this.UserPicture.Image = cachedImage.Clone();
+                return;
+            }
+
+            // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する
+            var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini");
+            if (fallbackImage != null)
+                this.UserPicture.Image = fallbackImage.Clone();
+
+            await this.UserPicture.SetImageFromTask(
+                async () =>
+                {
+                    var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize);
+                    var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
+                        .ConfigureAwait(false);
 
-                return await image.CloneAsync()
-                    .ConfigureAwait(false);
-            });
+                    return image.Clone();
+                },
+                useStatusImage: false
+            );
         }
 
         /// <summary>
@@ -282,7 +316,7 @@ namespace OpenTween
             var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
 
             using (ControlTransaction.Update(this.PostBrowser))
-                this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
+                this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
 
             // 引用ツイートを読み込み
             var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
@@ -299,7 +333,7 @@ namespace OpenTween
             body = post.Text + string.Concat(quoteHtmls);
 
             using (ControlTransaction.Update(this.PostBrowser))
-                this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
+                this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
         }
 
         private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
@@ -330,7 +364,7 @@ namespace OpenTween
             var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
                 " &mdash; " + WebUtility.HtmlEncode(post.Nickname) +
                 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
-                WebUtility.HtmlEncode(post.CreatedAt.ToString());
+                WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
 
             return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
         }
@@ -351,10 +385,7 @@ namespace OpenTween
         /// 指定されたHTMLからリンクを除去します
         /// </summary>
         internal static string StripLinkTagHtml(string html)
-        {
-            // a 要素はネストされていない前提の正規表現パターン
-            return Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1");
-        }
+            => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
 
         public async Task DoTranslation()
         {
@@ -366,7 +397,7 @@ namespace OpenTween
 
         private async Task DoTranslation(string str)
         {
-            if (string.IsNullOrEmpty(str))
+            if (MyCommon.IsNullOrEmpty(str))
                 return;
 
             var bing = new Bing();
@@ -376,9 +407,9 @@ namespace OpenTween
                     langFrom: null,
                     langTo: SettingManager.Common.TranslateLanguage);
 
-                this.PostBrowser.DocumentText = this.Owner.createDetailHtml(translatedText);
+                this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText);
             }
-            catch (HttpRequestException e)
+            catch (WebApiException e)
             {
                 this.RaiseStatusChanged("Err:" + e.Message);
             }
@@ -390,26 +421,26 @@ namespace OpenTween
 
         private async Task DoSearchToolStrip(string url)
         {
-            //発言詳細で「選択文字列で検索」(選択文字列取得)
-            string _selText = this.PostBrowser.GetSelectedText();
+            // 発言詳細で「選択文字列で検索」(選択文字列取得)
+            var selText = this.PostBrowser.GetSelectedText();
 
-            if (_selText != null)
+            if (selText != null)
             {
                 if (url == Properties.Resources.SearchItem4Url)
                 {
-                    //公式検索
-                    this.Owner.AddNewTabForSearch(_selText);
+                    // 公式検索
+                    this.Owner.AddNewTabForSearch(selText);
                     return;
                 }
 
-                string tmp = string.Format(url, Uri.EscapeDataString(_selText));
-                await this.Owner.OpenUriInBrowserAsync(tmp);
+                var tmp = string.Format(url, Uri.EscapeDataString(selText));
+                await MyCommon.OpenInBrowserAsync(this, tmp);
             }
         }
 
-        private string GetUserId()
+        private string? GetUserId()
         {
-            Match m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
+            var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
             if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
                 return m.Result("${ScreenName}");
             else
@@ -417,43 +448,31 @@ namespace OpenTween
         }
 
         protected void RaiseStatusChanged(string statusText)
-        {
-            this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
-        }
+            => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
 
         private void TweetDetailsView_FontChanged(object sender, EventArgs e)
         {
             // OTBaseForm.GlobalFont による UI フォントの変更に対応
-            var origFont = this.NameLabel.Font;
-            this.NameLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
+            var origFont = this.AuthorNameLinkLabel.Font;
+            this.AuthorNameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
+            this.RetweetedByLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
         }
 
         #region TableLayoutPanel1
 
-        private async void UserPicture_DoubleClick(object sender, EventArgs e)
-        {
-            if (this.CurrentPost == null)
-                return;
-
-            await this.Owner.OpenUriInBrowserAsync(MyCommon.TwitterUrl + this.CurrentPost.ScreenName);
-        }
-
-        private void UserPicture_MouseEnter(object sender, EventArgs e)
-        {
-            this.UserPicture.Cursor = Cursors.Hand;
-        }
-
-        private void UserPicture_MouseLeave(object sender, EventArgs e)
+        private async void UserPicture_Click(object sender, EventArgs e)
         {
-            this.UserPicture.Cursor = Cursors.Default;
+            var screenName = this.CurrentPost?.ScreenName;
+            if (screenName != null)
+                await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
         }
 
         private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
         {
             if (e.Url.AbsoluteUri != "about:blank")
             {
-                await this.ShowPostDetails(this.CurrentPost); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
-                await this.Owner.OpenUriInBrowserAsync(e.Url.OriginalString);
+                await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
+                await MyCommon.OpenInBrowserAsync(this, e.Url.OriginalString);
             }
         }
 
@@ -473,9 +492,8 @@ namespace OpenTween
 
         private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
         {
-            Task asyncTask;
-            bool KeyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out asyncTask);
-            if (KeyRes)
+            var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
+            if (keyRes)
             {
                 e.IsInputKey = true;
             }
@@ -507,13 +525,13 @@ namespace OpenTween
         {
             try
             {
-                if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
-                    || PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
-                    || PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
+                if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
+                    || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
+                    || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
                 {
                     this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
                 }
-                if (string.IsNullOrEmpty(PostBrowser.StatusText))
+                if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText))
                 {
                     this.RaiseStatusChanged(statusText: "");
                 }
@@ -528,7 +546,7 @@ namespace OpenTween
             var sourceUri = this.CurrentPost?.SourceUri;
             if (sourceUri != null && e.Button == MouseButtons.Left)
             {
-                await this.Owner.OpenUriInBrowserAsync(sourceUri.AbsoluteUri);
+                await MyCommon.OpenInBrowserAsync(this, sourceUri.AbsoluteUri);
             }
         }
 
@@ -542,9 +560,7 @@ namespace OpenTween
         }
 
         private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
-        {
-            this.RaiseStatusChanged(statusText: "");
-        }
+            => this.RaiseStatusChanged(statusText: "");
 
         #endregion
 
@@ -552,13 +568,13 @@ namespace OpenTween
 
         private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
         {
-            //発言詳細のアイコン右クリック時のメニュー制御
+            // 発言詳細のアイコン右クリック時のメニュー制御
             if (this.CurrentPost != null)
             {
-                string name = this.CurrentPost.ImageUrl;
-                if (!string.IsNullOrEmpty(name))
+                var name = this.CurrentPost.ImageUrl;
+                if (!MyCommon.IsNullOrEmpty(name))
                 {
-                    int idx = name.LastIndexOf('/');
+                    var idx = name.LastIndexOf('/');
                     if (idx != -1)
                     {
                         name = Path.GetFileName(name.Substring(idx));
@@ -610,34 +626,34 @@ namespace OpenTween
             {
                 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
                 {
-                    FollowToolStripMenuItem.Enabled = false;
-                    UnFollowToolStripMenuItem.Enabled = false;
-                    ShowFriendShipToolStripMenuItem.Enabled = false;
-                    ShowUserStatusToolStripMenuItem.Enabled = true;
-                    SearchPostsDetailNameToolStripMenuItem.Enabled = true;
-                    SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
-                    ListManageUserContextToolStripMenuItem3.Enabled = true;
+                    this.FollowToolStripMenuItem.Enabled = false;
+                    this.UnFollowToolStripMenuItem.Enabled = false;
+                    this.ShowFriendShipToolStripMenuItem.Enabled = false;
+                    this.ShowUserStatusToolStripMenuItem.Enabled = true;
+                    this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
+                    this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
+                    this.ListManageUserContextToolStripMenuItem3.Enabled = true;
                 }
                 else
                 {
-                    FollowToolStripMenuItem.Enabled = true;
-                    UnFollowToolStripMenuItem.Enabled = true;
-                    ShowFriendShipToolStripMenuItem.Enabled = true;
-                    ShowUserStatusToolStripMenuItem.Enabled = true;
-                    SearchPostsDetailNameToolStripMenuItem.Enabled = true;
-                    SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
-                    ListManageUserContextToolStripMenuItem3.Enabled = true;
+                    this.FollowToolStripMenuItem.Enabled = true;
+                    this.UnFollowToolStripMenuItem.Enabled = true;
+                    this.ShowFriendShipToolStripMenuItem.Enabled = true;
+                    this.ShowUserStatusToolStripMenuItem.Enabled = true;
+                    this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
+                    this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
+                    this.ListManageUserContextToolStripMenuItem3.Enabled = true;
                 }
             }
             else
             {
-                FollowToolStripMenuItem.Enabled = false;
-                UnFollowToolStripMenuItem.Enabled = false;
-                ShowFriendShipToolStripMenuItem.Enabled = false;
-                ShowUserStatusToolStripMenuItem.Enabled = false;
-                SearchPostsDetailNameToolStripMenuItem.Enabled = false;
-                SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
-                ListManageUserContextToolStripMenuItem3.Enabled = false;
+                this.FollowToolStripMenuItem.Enabled = false;
+                this.UnFollowToolStripMenuItem.Enabled = false;
+                this.ShowFriendShipToolStripMenuItem.Enabled = false;
+                this.ShowUserStatusToolStripMenuItem.Enabled = false;
+                this.SearchPostsDetailNameToolStripMenuItem.Enabled = false;
+                this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
+                this.ListManageUserContextToolStripMenuItem3.Enabled = false;
             }
         }
 
@@ -684,12 +700,12 @@ namespace OpenTween
             await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
         }
 
-        private void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
+        private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
         {
             if (this.CurrentPost == null)
                 return;
 
-            this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
+            await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
         }
 
         private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
@@ -702,17 +718,18 @@ namespace OpenTween
 
         private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            var imageUrl = this.CurrentPost?.ImageUrl;
-            if (string.IsNullOrEmpty(imageUrl))
+            var imageNormalUrl = this.CurrentPost?.ImageUrl;
+            if (MyCommon.IsNullOrEmpty(imageNormalUrl))
                 return;
 
-            await this.Owner.OpenUriInBrowserAsync(imageUrl.Remove(imageUrl.LastIndexOf("_normal", StringComparison.Ordinal), 7)); // "_normal".Length
+            var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original");
+            await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl);
         }
 
         private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
         {
             var imageUrl = this.CurrentPost?.ImageUrl;
-            if (string.IsNullOrEmpty(imageUrl))
+            if (MyCommon.IsNullOrEmpty(imageUrl))
                 return;
 
             await this.SetUserPictureAsync(imageUrl, force: true);
@@ -721,7 +738,11 @@ namespace OpenTween
         private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
         {
             var imageUrl = this.CurrentPost?.ImageUrl;
-            if (string.IsNullOrEmpty(imageUrl))
+            if (MyCommon.IsNullOrEmpty(imageUrl))
+                return;
+
+            var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
+            if (memoryImage == null)
                 return;
 
             this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
@@ -730,22 +751,19 @@ namespace OpenTween
             {
                 try
                 {
-                    using (Image orgBmp = new Bitmap(IconCache.TryGetFromCache(imageUrl).Image))
+                    using var orgBmp = new Bitmap(memoryImage.Image);
+                    using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
+
+                    using (var g = Graphics.FromImage(bmp2))
                     {
-                        using (Bitmap bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height))
-                        {
-                            using (Graphics g = Graphics.FromImage(bmp2))
-                            {
-                                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
-                                g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
-                            }
-                            bmp2.Save(this.Owner.SaveFileDialog1.FileName);
-                        }
+                        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
+                        g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
                     }
+                    bmp2.Save(this.Owner.SaveFileDialog1.FileName);
                 }
                 catch (Exception)
                 {
-                    //処理中にキャッシュアウトする可能性あり
+                    // 処理中にキャッシュアウトする可能性あり
                 }
             }
         }
@@ -757,73 +775,73 @@ namespace OpenTween
         private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
         {
             // URLコピーの項目の表示/非表示
-            if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
+            if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
             {
-                this._postBrowserStatusText = PostBrowser.StatusText;
-                string name = GetUserId();
-                UrlCopyContextMenuItem.Enabled = true;
+                this.postBrowserStatusText = this.PostBrowser.StatusText;
+                var name = this.GetUserId();
+                this.UrlCopyContextMenuItem.Enabled = true;
                 if (name != null)
                 {
-                    FollowContextMenuItem.Enabled = true;
-                    RemoveContextMenuItem.Enabled = true;
-                    FriendshipContextMenuItem.Enabled = true;
-                    ShowUserStatusContextMenuItem.Enabled = true;
-                    SearchPostsDetailToolStripMenuItem.Enabled = true;
-                    IdFilterAddMenuItem.Enabled = true;
-                    ListManageUserContextToolStripMenuItem.Enabled = true;
-                    SearchAtPostsDetailToolStripMenuItem.Enabled = true;
+                    this.FollowContextMenuItem.Enabled = true;
+                    this.RemoveContextMenuItem.Enabled = true;
+                    this.FriendshipContextMenuItem.Enabled = true;
+                    this.ShowUserStatusContextMenuItem.Enabled = true;
+                    this.SearchPostsDetailToolStripMenuItem.Enabled = true;
+                    this.IdFilterAddMenuItem.Enabled = true;
+                    this.ListManageUserContextToolStripMenuItem.Enabled = true;
+                    this.SearchAtPostsDetailToolStripMenuItem.Enabled = true;
                 }
                 else
                 {
-                    FollowContextMenuItem.Enabled = false;
-                    RemoveContextMenuItem.Enabled = false;
-                    FriendshipContextMenuItem.Enabled = false;
-                    ShowUserStatusContextMenuItem.Enabled = false;
-                    SearchPostsDetailToolStripMenuItem.Enabled = false;
-                    IdFilterAddMenuItem.Enabled = false;
-                    ListManageUserContextToolStripMenuItem.Enabled = false;
-                    SearchAtPostsDetailToolStripMenuItem.Enabled = false;
+                    this.FollowContextMenuItem.Enabled = false;
+                    this.RemoveContextMenuItem.Enabled = false;
+                    this.FriendshipContextMenuItem.Enabled = false;
+                    this.ShowUserStatusContextMenuItem.Enabled = false;
+                    this.SearchPostsDetailToolStripMenuItem.Enabled = false;
+                    this.IdFilterAddMenuItem.Enabled = false;
+                    this.ListManageUserContextToolStripMenuItem.Enabled = false;
+                    this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
                 }
 
-                if (Regex.IsMatch(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
-                    UseHashtagMenuItem.Enabled = true;
+                if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
+                    this.UseHashtagMenuItem.Enabled = true;
                 else
-                    UseHashtagMenuItem.Enabled = false;
+                    this.UseHashtagMenuItem.Enabled = false;
             }
             else
             {
-                this._postBrowserStatusText = "";
-                UrlCopyContextMenuItem.Enabled = false;
-                FollowContextMenuItem.Enabled = false;
-                RemoveContextMenuItem.Enabled = false;
-                FriendshipContextMenuItem.Enabled = false;
-                ShowUserStatusContextMenuItem.Enabled = false;
-                SearchPostsDetailToolStripMenuItem.Enabled = false;
-                SearchAtPostsDetailToolStripMenuItem.Enabled = false;
-                UseHashtagMenuItem.Enabled = false;
-                IdFilterAddMenuItem.Enabled = false;
-                ListManageUserContextToolStripMenuItem.Enabled = false;
+                this.postBrowserStatusText = "";
+                this.UrlCopyContextMenuItem.Enabled = false;
+                this.FollowContextMenuItem.Enabled = false;
+                this.RemoveContextMenuItem.Enabled = false;
+                this.FriendshipContextMenuItem.Enabled = false;
+                this.ShowUserStatusContextMenuItem.Enabled = false;
+                this.SearchPostsDetailToolStripMenuItem.Enabled = false;
+                this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
+                this.UseHashtagMenuItem.Enabled = false;
+                this.IdFilterAddMenuItem.Enabled = false;
+                this.ListManageUserContextToolStripMenuItem.Enabled = false;
             }
             // 文字列選択されていないときは選択文字列関係の項目を非表示に
-            string _selText = this.PostBrowser.GetSelectedText();
-            if (_selText == null)
+            var selText = this.PostBrowser.GetSelectedText();
+            if (selText == null)
             {
-                SelectionSearchContextMenuItem.Enabled = false;
-                SelectionCopyContextMenuItem.Enabled = false;
-                SelectionTranslationToolStripMenuItem.Enabled = false;
+                this.SelectionSearchContextMenuItem.Enabled = false;
+                this.SelectionCopyContextMenuItem.Enabled = false;
+                this.SelectionTranslationToolStripMenuItem.Enabled = false;
             }
             else
             {
-                SelectionSearchContextMenuItem.Enabled = true;
-                SelectionCopyContextMenuItem.Enabled = true;
-                SelectionTranslationToolStripMenuItem.Enabled = true;
+                this.SelectionSearchContextMenuItem.Enabled = true;
+                this.SelectionCopyContextMenuItem.Enabled = true;
+                this.SelectionTranslationToolStripMenuItem.Enabled = true;
             }
-            //発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
-            MatchCollection ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
-            bool fAllFlag = false;
+            // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
+            var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
+            var fAllFlag = false;
             foreach (Match mu in ma)
             {
-                if (mu.Result("${ScreenName}").ToLowerInvariant() != this.Owner.TwitterInstance.Username.ToLowerInvariant())
+                if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
                 {
                     fAllFlag = true;
                     break;
@@ -832,41 +850,36 @@ namespace OpenTween
             this.FriendshipAllMenuItem.Enabled = fAllFlag;
 
             if (this.CurrentPost == null)
-                TranslationToolStripMenuItem.Enabled = false;
+                this.TranslationToolStripMenuItem.Enabled = false;
             else
-                TranslationToolStripMenuItem.Enabled = true;
+                this.TranslationToolStripMenuItem.Enabled = true;
 
             e.Cancel = false;
         }
 
         private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
-        {
-            await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
-        }
+            => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
 
         private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
-        {
-            await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
-        }
+            => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
 
         private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
-        {
-            await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
-        }
+            => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
 
         private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            //発言詳細の選択文字列で現在のタブを検索
-            string _selText = this.PostBrowser.GetSelectedText();
+            // 発言詳細の選択文字列で現在のタブを検索
+            var selText = this.PostBrowser.GetSelectedText();
 
-            if (_selText != null)
+            if (selText != null)
             {
                 var searchOptions = new SearchWordDialog.SearchOptions(
                     SearchWordDialog.SearchType.Timeline,
-                    _selText,
-                    newTab: false,
-                    caseSensitive: false,
-                    useRegex: false);
+                    selText,
+                    NewTab: false,
+                    CaseSensitive: false,
+                    UseRegex: false
+                );
 
                 this.Owner.SearchDialog.ResultOptions = searchOptions;
 
@@ -880,11 +893,11 @@ namespace OpenTween
 
         private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
         {
-            //発言詳細で「選択文字列をコピー」
-            string _selText = this.PostBrowser.GetSelectedText();
+            // 発言詳細で「選択文字列をコピー」
+            var selText = this.PostBrowser.GetSelectedText();
             try
             {
-                Clipboard.SetDataObject(_selText, false, 5, 100);
+                Clipboard.SetDataObject(selText, false, 5, 100);
             }
             catch (Exception ex)
             {
@@ -898,10 +911,10 @@ namespace OpenTween
             {
                 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
                 {
-                    if (link.GetAttribute("href") == this._postBrowserStatusText)
+                    if (link.GetAttribute("href") == this.postBrowserStatusText)
                     {
                         var linkStr = link.GetAttribute("title");
-                        if (string.IsNullOrEmpty(linkStr))
+                        if (MyCommon.IsNullOrEmpty(linkStr))
                             linkStr = link.GetAttribute("href");
 
                         Clipboard.SetDataObject(linkStr, false, 5, 100);
@@ -909,7 +922,7 @@ namespace OpenTween
                     }
                 }
 
-                Clipboard.SetDataObject(this._postBrowserStatusText, false, 5, 100);
+                Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100);
             }
             catch (Exception ex)
             {
@@ -918,39 +931,36 @@ namespace OpenTween
         }
 
         private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
-        {
-            //発言詳細ですべて選択
-            PostBrowser.Document.ExecCommand("SelectAll", false, null);
-        }
+            => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
 
         private async void FollowContextMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null)
                 await this.Owner.FollowCommand(name);
         }
 
         private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null)
                 await this.Owner.RemoveCommand(name, false);
         }
 
         private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null)
                 await this.Owner.ShowFriendship(name);
         }
 
         private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
         {
-            MatchCollection ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
-            List<string> ids = new List<string>();
+            var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
+            var ids = new List<string>();
             foreach (Match mu in ma)
             {
-                if (mu.Result("${ScreenName}").ToLower() != this.Owner.TwitterInstance.Username.ToLower())
+                if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
                 {
                     ids.Add(mu.Result("${ScreenName}"));
                 }
@@ -961,38 +971,39 @@ namespace OpenTween
 
         private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null)
                 await this.Owner.ShowUserStatus(name);
         }
 
-        private void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
+        private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
-            if (name != null) this.Owner.AddNewTabForUserTimeline(name);
+            var name = this.GetUserId();
+            if (name != null)
+                await this.Owner.AddNewTabForUserTimeline(name);
         }
 
         private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null) this.Owner.AddNewTabForSearch("@" + name);
         }
 
         private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
         {
-            string name = GetUserId();
+            var name = this.GetUserId();
             if (name != null)
                 this.Owner.AddFilterRuleByScreenName(name);
         }
 
         private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
         {
-            ToolStripMenuItem menuItem = (ToolStripMenuItem)sender;
+            var menuItem = (ToolStripMenuItem)sender;
 
-            string user;
+            string? user;
             if (menuItem.Owner == this.ContextMenuPostBrowser)
             {
-                user = GetUserId();
+                user = this.GetUserId();
                 if (user == null) return;
             }
             else if (this.CurrentPost != null)
@@ -1009,7 +1020,7 @@ namespace OpenTween
 
         private void UseHashtagMenuItem_Click(object sender, EventArgs e)
         {
-            Match m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
+            var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
             if (m.Success)
                 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
         }
@@ -1021,9 +1032,7 @@ namespace OpenTween
         }
 
         private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
-        {
-            await this.DoTranslation();
-        }
+            => await this.DoTranslation();
 
         #endregion
 
@@ -1033,13 +1042,13 @@ namespace OpenTween
         {
             if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
             {
-                SourceCopyMenuItem.Enabled = false;
-                SourceUrlCopyMenuItem.Enabled = false;
+                this.SourceCopyMenuItem.Enabled = false;
+                this.SourceUrlCopyMenuItem.Enabled = false;
             }
             else
             {
-                SourceCopyMenuItem.Enabled = true;
-                SourceUrlCopyMenuItem.Enabled = true;
+                this.SourceCopyMenuItem.Enabled = true;
+                this.SourceUrlCopyMenuItem.Enabled = true;
             }
         }
 
@@ -1075,6 +1084,20 @@ namespace OpenTween
         }
 
         #endregion
+
+        private async void AuthorNameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
+        {
+            var screenName = this.CurrentPost?.ScreenName;
+            if (screenName != null)
+                await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
+        }
+
+        private async void RetweetedByLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
+        {
+            var screenName = this.CurrentPost?.RetweetedBy;
+            if (screenName != null)
+                await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
+        }
     }
 
     public class TweetDetailsViewStatusChengedEventArgs : EventArgs
@@ -1086,8 +1109,6 @@ namespace OpenTween
         public string StatusText { get; }
 
         public TweetDetailsViewStatusChengedEventArgs(string statusText)
-        {
-            this.StatusText = statusText;
-        }
+            => this.StatusText = statusText;
     }
 }