1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
10 // This file is part of OpenTween.
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
22 // You should have received a copy of the GNU General public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
30 using System.Collections.Generic;
31 using System.ComponentModel;
37 using System.Net.Http;
39 using System.Text.RegularExpressions;
40 using System.Threading;
41 using System.Threading.Tasks;
42 using System.Windows.Forms;
43 using OpenTween.Models;
44 using OpenTween.Setting;
45 using OpenTween.SocialProtocol.Twitter;
49 public partial class TweetDetailsView : UserControl
51 private TweenMain Owner
52 => this.owner ?? throw this.NotInitializedException();
54 /// <summary>プロフィール画像のキャッシュ</summary>
55 private ImageCache IconCache
56 => this.iconCache ?? throw this.NotInitializedException();
58 private DetailsHtmlBuilder HtmlBuilder
59 => this.detailsHtmlBuilder ?? throw this.NotInitializedException();
61 /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
63 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
64 public bool DumpPostClass { get; set; }
66 /// <summary>現在表示中の発言</summary>
67 public PostClass? CurrentPost { get; private set; }
70 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
71 public ThemeManager Theme
73 get => this.themeManager ?? throw this.NotInitializedException();
74 set => this.themeManager = value;
78 public new bool TabStop
81 set => base.TabStop = value;
84 /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
85 public event EventHandler<TweetDetailsViewStatusChengedEventArgs>? StatusChanged;
87 /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
88 private string postBrowserStatusText = "";
90 private TweenMain? owner;
91 private ImageCache? iconCache;
92 private ThemeManager? themeManager;
93 private DetailsHtmlBuilder? detailsHtmlBuilder;
95 public TweetDetailsView()
97 this.InitializeComponent();
102 this.NameLinkLabel.Text = "";
103 this.DateTimeLabel.Text = "";
104 this.SourceLinkLabel.Text = "";
106 new InternetSecurityManager(this.PostBrowser);
107 this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
110 public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager, DetailsHtmlBuilder detailsHtmlBuilder)
113 this.iconCache = iconCache;
114 this.themeManager = themeManager;
115 this.detailsHtmlBuilder = detailsHtmlBuilder;
118 private Exception NotInitializedException()
119 => new InvalidOperationException("Cannot call before initialization");
121 public void ClearPostBrowser()
122 => this.PostBrowser.DocumentText = this.HtmlBuilder.Build("");
124 public async Task ShowPostDetails(PostClass post)
126 this.CurrentPost = post;
128 var loadTasks = new TaskCollection();
130 using (ControlTransaction.Update(this.TableLayoutPanel1))
132 this.SourceLinkLabel.Text = post.Source;
133 this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
139 nameText = "DM FROM <- ";
141 nameText = "DM TO -> ";
147 nameText += post.ScreenName + "/" + post.Nickname;
148 if (post.RetweetedId != null)
149 nameText += $" (RT:{post.RetweetedBy})";
151 this.NameLinkLabel.Text = nameText;
153 var nameForeColor = SystemColors.ControlText;
154 if (post.IsOwl && (SettingManager.Instance.Common.OneWayLove || post.IsDm))
155 nameForeColor = this.Theme.ColorOWL;
156 if (post.RetweetedId != null)
157 nameForeColor = this.Theme.ColorRetweet;
159 nameForeColor = this.Theme.ColorFav;
161 this.NameLinkLabel.LinkColor = nameForeColor;
162 this.NameLinkLabel.ActiveLinkColor = nameForeColor;
164 loadTasks.Add(() => this.SetUserPictureAsync(post.ImageUrl));
166 this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
169 if (this.DumpPostClass)
171 var sb = new StringBuilder(512);
173 sb.Append("-----Start PostClass Dump<br>");
174 sb.AppendFormat("TextFromApi : {0}<br>", post.TextFromApi);
175 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.TextFromApi);
176 sb.AppendFormat("StatusId : {0}<br>", post.StatusId);
177 sb.AppendFormat("ImageUrl : {0}<br>", post.ImageUrl);
178 sb.AppendFormat("InReplyToStatusId : {0}<br>", post.InReplyToStatusId);
179 sb.AppendFormat("InReplyToUser : {0}<br>", post.InReplyToUser);
180 sb.AppendFormat("IsDM : {0}<br>", post.IsDm);
181 sb.AppendFormat("IsFav : {0}<br>", post.IsFav);
182 sb.AppendFormat("IsMark : {0}<br>", post.IsMark);
183 sb.AppendFormat("IsMe : {0}<br>", post.IsMe);
184 sb.AppendFormat("IsOwl : {0}<br>", post.IsOwl);
185 sb.AppendFormat("IsProtect : {0}<br>", post.IsProtect);
186 sb.AppendFormat("IsRead : {0}<br>", post.IsRead);
187 sb.AppendFormat("IsReply : {0}<br>", post.IsReply);
189 foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
191 sb.AppendFormat("ReplyToList : {0}<br>", nm);
194 sb.AppendFormat("ScreenName : {0}<br>", post.ScreenName);
195 sb.AppendFormat("NickName : {0}<br>", post.Nickname);
196 sb.AppendFormat("Text : {0}<br>", post.Text);
197 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.Text);
198 sb.AppendFormat("CreatedAt : {0}<br>", post.CreatedAt.ToLocalTimeString());
199 sb.AppendFormat("Source : {0}<br>", post.Source);
200 sb.AppendFormat("UserId : {0}<br>", post.UserId);
201 sb.AppendFormat("FilterHit : {0}<br>", post.FilterHit);
202 sb.AppendFormat("RetweetedBy : {0}<br>", post.RetweetedBy);
203 sb.AppendFormat("RetweetedId : {0}<br>", post.RetweetedId);
205 sb.AppendFormat("Media.Count : {0}<br>", post.Media.Count);
206 if (post.Media.Count > 0)
208 for (var i = 0; i < post.Media.Count; i++)
210 var info = post.Media[i];
211 sb.AppendFormat("Media[{0}].Url : {1}<br>", i, info.Url);
212 sb.AppendFormat("Media[{0}].VideoUrl : {1}<br>", i, info.VideoUrl ?? "---");
215 sb.Append("-----End PostClass Dump<br>");
217 this.PostBrowser.DocumentText = this.HtmlBuilder.Build(sb.ToString());
221 using (ControlTransaction.Update(this.PostBrowser))
223 this.PostBrowser.DocumentText =
224 this.HtmlBuilder.Build(post.IsDeleted ? "(DELETED)" : post.Text);
226 this.PostBrowser.Document.Window.ScrollTo(0, 0);
229 loadTasks.Add(() => this.AppendQuoteTweetAsync(post));
231 await loadTasks.RunAll();
234 public void ScrollDownPostBrowser(bool forward)
236 var doc = this.PostBrowser.Document;
237 if (doc == null) return;
239 var tags = doc.GetElementsByTagName("html");
243 tags[0].ScrollTop += this.Theme.FontDetail.Height;
245 tags[0].ScrollTop -= this.Theme.FontDetail.Height;
249 public void PageDownPostBrowser(bool forward)
251 var doc = this.PostBrowser.Document;
252 if (doc == null) return;
254 var tags = doc.GetElementsByTagName("html");
258 tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height;
260 tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height;
264 public HtmlElement[] GetLinkElements()
266 return this.PostBrowser.Document.Links.Cast<HtmlElement>()
267 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
271 private async Task SetUserPictureAsync(string normalImageUrl, bool force = false)
273 if (MyCommon.IsNullOrEmpty(normalImageUrl))
276 if (this.IconCache == null)
279 this.ClearUserPicture();
281 var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width);
284 var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize);
285 if (cachedImage != null)
287 // 既にキャッシュされていればそれを表示して終了
288 this.UserPicture.Image = cachedImage.Clone();
292 // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する
293 var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini");
294 if (fallbackImage != null)
295 this.UserPicture.Image = fallbackImage.Clone();
298 await this.UserPicture.SetImageFromTask(
301 var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize);
302 var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
303 .ConfigureAwait(false);
305 return image.Clone();
307 useStatusImage: false
312 /// UserPicture.Image に設定されている画像を破棄します。
314 private void ClearUserPicture()
316 if (this.UserPicture.Image != null)
318 var oldImage = this.UserPicture.Image;
319 this.UserPicture.Image = null;
325 /// 発言詳細欄のツイートURLを展開する
327 private async Task AppendQuoteTweetAsync(PostClass post)
329 var quoteStatusIds = post.QuoteStatusIds;
330 if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
334 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
336 var loadingReplyHtml = string.Empty;
337 if (post.InReplyToStatusId != null)
338 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId, Properties.Resources.LoadingText, isReply: true);
340 var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
342 using (ControlTransaction.Update(this.PostBrowser))
343 this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body);
346 var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
348 if (post.InReplyToStatusId != null)
349 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId, isReply: true));
351 var quoteHtmls = await Task.WhenAll(loadTweetTasks);
353 // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
354 if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
357 body = post.Text + string.Concat(quoteHtmls);
359 using (ControlTransaction.Update(this.PostBrowser))
360 this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body);
363 private async Task<string> CreateQuoteTweetHtml(PostId statusId, bool isReply)
365 var post = TabInformations.GetInstance()[statusId];
368 var account = this.Owner.GetAccountForPostId(statusId);
370 return FormatQuoteTweetHtml(statusId, "This post is unavailable.", isReply);
374 post = await account.Client.GetPostById(statusId, firstLoad: false)
375 .ConfigureAwait(false);
377 catch (WebApiException ex)
379 return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
382 if (account.AccountState.BlockedUserIds.Contains(post.UserId))
383 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
385 if (!TabInformations.GetInstance().AddQuoteTweet(post))
386 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
389 return FormatQuoteTweetHtml(post, isReply);
392 internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
394 var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
395 " — " + WebUtility.HtmlEncode(post.Nickname) +
396 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
397 WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
399 return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
402 internal static string FormatQuoteTweetHtml(PostId statusId, string innerHtml, bool isReply)
404 var blockClassName = "quote-tweet";
407 blockClassName += " reply";
409 return $"""<a class="quote-tweet-link" href="//opentween/status/{statusId.Id}">""" +
410 $"""<blockquote class="{blockClassName}">{innerHtml}</blockquote>""" +
415 /// 指定されたHTMLからリンクを除去します
417 internal static string StripLinkTagHtml(string html)
418 => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
420 public async Task DoTranslation()
422 if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
425 await this.DoTranslation(this.CurrentPost.TextFromApi);
428 private async Task DoTranslation(string str)
430 if (MyCommon.IsNullOrEmpty(str))
433 var bing = new Bing();
436 var translatedText = await bing.TranslateAsync(str,
438 langTo: SettingManager.Instance.Common.TranslateLanguage);
440 this.PostBrowser.DocumentText = this.HtmlBuilder.Build(translatedText);
442 catch (WebApiException e)
444 this.RaiseStatusChanged("Err:" + e.Message);
446 catch (OperationCanceledException)
448 this.RaiseStatusChanged("Err:Timeout");
452 private async Task DoSearchToolStrip(string url)
454 // 発言詳細で「選択文字列で検索」(選択文字列取得)
455 var selText = this.PostBrowser.GetSelectedText();
459 if (url == Properties.Resources.SearchItem4Url)
462 this.Owner.AddNewTabForSearch(selText);
466 var tmp = string.Format(url, Uri.EscapeDataString(selText));
467 await MyCommon.OpenInBrowserAsync(this, tmp);
471 private string? GetUserId()
473 var m = Twitter.StatusUrlRegex.Match(this.postBrowserStatusText);
474 if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
475 return m.Result("${ScreenName}");
480 protected void RaiseStatusChanged(string statusText)
481 => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
483 private void TweetDetailsView_FontChanged(object sender, EventArgs e)
485 // OTBaseForm.GlobalFont による UI フォントの変更に対応
486 var origFont = this.NameLinkLabel.Font;
487 this.NameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
490 #region TableLayoutPanel1
492 private async void UserPicture_Click(object sender, EventArgs e)
494 var screenName = this.CurrentPost?.ScreenName;
495 if (screenName != null)
496 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
499 private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
501 if (e.Url.AbsoluteUri != "about:blank")
503 await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
504 await MyCommon.OpenInBrowserAsync(this, e.Url);
508 private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
510 if (e.Url.Scheme == "data")
512 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
514 else if (e.Url.AbsoluteUri != "about:blank")
517 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
518 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
522 private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
524 var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
531 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
533 var shortcut = (Shortcut)e.KeyData;
538 case Shortcut.CtrlIns:
542 // その他のショートカットキーは無効にする
549 if (asyncTask != null)
553 private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
557 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
558 || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
559 || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
561 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
563 if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText))
565 this.RaiseStatusChanged(statusText: "");
573 private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
575 var sourceUri = this.CurrentPost?.SourceUri;
576 if (sourceUri != null && e.Button == MouseButtons.Left)
578 await MyCommon.OpenInBrowserAsync(this, sourceUri);
582 private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
584 var sourceUri = this.CurrentPost?.SourceUri;
585 if (sourceUri != null)
587 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
591 private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
592 => this.RaiseStatusChanged(statusText: "");
596 #region ContextMenuUserPicture
598 private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
600 // 発言詳細のアイコン右クリック時のメニュー制御
601 if (this.CurrentPost != null)
603 var name = this.CurrentPost.ImageUrl;
604 if (!MyCommon.IsNullOrEmpty(name))
606 var idx = name.LastIndexOf('/');
609 name = Path.GetFileName(name.Substring(idx));
610 if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
612 name = name.Replace("_normal", "");
613 this.IconNameToolStripMenuItem.Text = name;
614 this.IconNameToolStripMenuItem.Enabled = true;
618 this.IconNameToolStripMenuItem.Enabled = false;
619 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
624 this.IconNameToolStripMenuItem.Enabled = false;
625 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
628 this.ReloadIconToolStripMenuItem.Enabled = true;
630 if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
632 this.SaveIconPictureToolStripMenuItem.Enabled = true;
636 this.SaveIconPictureToolStripMenuItem.Enabled = false;
641 this.IconNameToolStripMenuItem.Enabled = false;
642 this.ReloadIconToolStripMenuItem.Enabled = false;
643 this.SaveIconPictureToolStripMenuItem.Enabled = false;
644 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
649 this.IconNameToolStripMenuItem.Enabled = false;
650 this.ReloadIconToolStripMenuItem.Enabled = false;
651 this.SaveIconPictureToolStripMenuItem.Enabled = false;
652 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
654 if (this.CurrentPost != null)
656 if (this.CurrentPost.UserId == this.Owner.CurrentTabAccount.UserId)
658 this.FollowToolStripMenuItem.Enabled = false;
659 this.UnFollowToolStripMenuItem.Enabled = false;
660 this.ShowFriendShipToolStripMenuItem.Enabled = false;
661 this.ShowUserStatusToolStripMenuItem.Enabled = true;
662 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
663 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
664 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
668 this.FollowToolStripMenuItem.Enabled = true;
669 this.UnFollowToolStripMenuItem.Enabled = true;
670 this.ShowFriendShipToolStripMenuItem.Enabled = true;
671 this.ShowUserStatusToolStripMenuItem.Enabled = true;
672 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
673 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
674 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
679 this.FollowToolStripMenuItem.Enabled = false;
680 this.UnFollowToolStripMenuItem.Enabled = false;
681 this.ShowFriendShipToolStripMenuItem.Enabled = false;
682 this.ShowUserStatusToolStripMenuItem.Enabled = false;
683 this.SearchPostsDetailNameToolStripMenuItem.Enabled = false;
684 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
685 this.ListManageUserContextToolStripMenuItem3.Enabled = false;
689 private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
691 if (this.CurrentPost == null)
694 if (this.CurrentPost.UserId == this.Owner.CurrentTabAccount.UserId)
697 await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
700 private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
702 if (this.CurrentPost == null)
705 if (this.CurrentPost.UserId == this.Owner.CurrentTabAccount.UserId)
708 await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
711 private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
713 if (this.CurrentPost == null)
716 if (this.CurrentPost.UserId == this.Owner.CurrentTabAccount.UserId)
719 await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
722 // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
724 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
726 if (this.CurrentPost == null)
729 await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
732 private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
734 if (this.CurrentPost == null)
737 await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
740 private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
742 if (this.CurrentPost == null)
745 this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
748 private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
750 var imageNormalUrl = this.CurrentPost?.ImageUrl;
751 if (MyCommon.IsNullOrEmpty(imageNormalUrl))
754 var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original");
755 await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl);
758 private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
760 var imageUrl = this.CurrentPost?.ImageUrl;
761 if (MyCommon.IsNullOrEmpty(imageUrl))
764 await this.SetUserPictureAsync(imageUrl, force: true);
767 private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
769 var imageUrl = this.CurrentPost?.ImageUrl;
770 if (MyCommon.IsNullOrEmpty(imageUrl))
773 var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
774 if (memoryImage == null)
777 this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
779 if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
783 using var orgBmp = new Bitmap(memoryImage.Image);
784 using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
786 using (var g = Graphics.FromImage(bmp2))
788 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
789 g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
791 bmp2.Save(this.Owner.SaveFileDialog1.FileName);
795 // 処理中にキャッシュアウトする可能性あり
802 #region ContextMenuPostBrowser
804 private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
807 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
809 this.postBrowserStatusText = this.PostBrowser.StatusText;
810 var name = this.GetUserId();
811 this.UrlCopyContextMenuItem.Enabled = true;
814 this.FollowContextMenuItem.Enabled = true;
815 this.RemoveContextMenuItem.Enabled = true;
816 this.FriendshipContextMenuItem.Enabled = true;
817 this.ShowUserStatusContextMenuItem.Enabled = true;
818 this.SearchPostsDetailToolStripMenuItem.Enabled = true;
819 this.IdFilterAddMenuItem.Enabled = true;
820 this.ListManageUserContextToolStripMenuItem.Enabled = true;
821 this.SearchAtPostsDetailToolStripMenuItem.Enabled = true;
825 this.FollowContextMenuItem.Enabled = false;
826 this.RemoveContextMenuItem.Enabled = false;
827 this.FriendshipContextMenuItem.Enabled = false;
828 this.ShowUserStatusContextMenuItem.Enabled = false;
829 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
830 this.IdFilterAddMenuItem.Enabled = false;
831 this.ListManageUserContextToolStripMenuItem.Enabled = false;
832 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
835 if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://(twitter|x).com/search\?q=%23"))
836 this.UseHashtagMenuItem.Enabled = true;
838 this.UseHashtagMenuItem.Enabled = false;
842 this.postBrowserStatusText = "";
843 this.UrlCopyContextMenuItem.Enabled = false;
844 this.FollowContextMenuItem.Enabled = false;
845 this.RemoveContextMenuItem.Enabled = false;
846 this.FriendshipContextMenuItem.Enabled = false;
847 this.ShowUserStatusContextMenuItem.Enabled = false;
848 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
849 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
850 this.UseHashtagMenuItem.Enabled = false;
851 this.IdFilterAddMenuItem.Enabled = false;
852 this.ListManageUserContextToolStripMenuItem.Enabled = false;
854 // 文字列選択されていないときは選択文字列関係の項目を非表示に
855 var selText = this.PostBrowser.GetSelectedText();
858 this.SelectionSearchContextMenuItem.Enabled = false;
859 this.SelectionCopyContextMenuItem.Enabled = false;
860 this.SelectionTranslationToolStripMenuItem.Enabled = false;
864 this.SelectionSearchContextMenuItem.Enabled = true;
865 this.SelectionCopyContextMenuItem.Enabled = true;
866 this.SelectionTranslationToolStripMenuItem.Enabled = true;
868 // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
869 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://(twitter|x).com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
870 var fAllFlag = false;
871 foreach (Match mu in ma)
873 if (!mu.Result("${ScreenName}").Equals(this.Owner.CurrentTabAccount.UserName, StringComparison.InvariantCultureIgnoreCase))
879 this.FriendshipAllMenuItem.Enabled = fAllFlag;
881 if (this.CurrentPost == null)
882 this.TranslationToolStripMenuItem.Enabled = false;
884 this.TranslationToolStripMenuItem.Enabled = true;
889 private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
890 => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
892 private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
893 => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
895 private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
896 => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
898 private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
900 // 発言詳細の選択文字列で現在のタブを検索
901 var selText = this.PostBrowser.GetSelectedText();
905 var searchOptions = new SearchWordDialog.SearchOptions(
906 SearchWordDialog.SearchType.Timeline,
909 CaseSensitive: false,
913 this.Owner.SearchDialog.ResultOptions = searchOptions;
915 this.Owner.DoTabSearch(
917 searchOptions.CaseSensitive,
918 searchOptions.UseRegex,
919 TweenMain.SEARCHTYPE.NextSearch);
923 private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
926 var selText = this.PostBrowser.GetSelectedText();
929 Clipboard.SetDataObject(selText, false, 5, 100);
933 MessageBox.Show(ex.Message);
937 private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
941 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
943 if (link.GetAttribute("href") == this.postBrowserStatusText)
945 var linkStr = link.GetAttribute("title");
946 if (MyCommon.IsNullOrEmpty(linkStr))
947 linkStr = link.GetAttribute("href");
949 Clipboard.SetDataObject(linkStr, false, 5, 100);
954 Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100);
958 MessageBox.Show(ex.Message);
962 private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
963 => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
965 private async void FollowContextMenuItem_Click(object sender, EventArgs e)
967 var name = this.GetUserId();
969 await this.Owner.FollowCommand(name);
972 private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
974 var name = this.GetUserId();
976 await this.Owner.RemoveCommand(name, false);
979 private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
981 var name = this.GetUserId();
983 await this.Owner.ShowFriendship(name);
986 private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
988 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://(twitter|x).com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
989 var ids = new List<string>();
990 foreach (Match mu in ma)
992 if (!mu.Result("${ScreenName}").Equals(this.Owner.CurrentTabAccount.UserName, StringComparison.InvariantCultureIgnoreCase))
994 ids.Add(mu.Result("${ScreenName}"));
998 await this.Owner.ShowFriendship(ids.ToArray());
1001 private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
1003 var name = this.GetUserId();
1005 await this.Owner.ShowUserStatus(name);
1008 private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
1010 var name = this.GetUserId();
1012 await this.Owner.AddNewTabForUserTimeline(name);
1015 private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
1017 var name = this.GetUserId();
1018 if (name != null) this.Owner.AddNewTabForSearch("@" + name);
1021 private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
1023 var name = this.GetUserId();
1025 this.Owner.AddFilterRuleByScreenName(name);
1028 private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
1030 var menuItem = (ToolStripMenuItem)sender;
1033 if (menuItem.Owner == this.ContextMenuPostBrowser)
1035 user = this.GetUserId();
1036 if (user == null) return;
1038 else if (this.CurrentPost != null)
1040 user = this.CurrentPost.ScreenName;
1047 this.Owner.ListManageUserContext(user);
1050 private void UseHashtagMenuItem_Click(object sender, EventArgs e)
1052 var m = Regex.Match(this.postBrowserStatusText, @"^https?://(twitter|x).com/search\?q=%23(?<hash>.+)$");
1054 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1057 private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1059 var text = this.PostBrowser.GetSelectedText();
1060 await this.DoTranslation(text);
1063 private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1064 => await this.DoTranslation();
1068 #region ContextMenuSource
1070 private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1072 if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1074 this.SourceCopyMenuItem.Enabled = false;
1075 this.SourceUrlCopyMenuItem.Enabled = false;
1079 this.SourceCopyMenuItem.Enabled = true;
1080 this.SourceUrlCopyMenuItem.Enabled = true;
1084 private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1086 if (this.CurrentPost == null)
1091 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1093 catch (Exception ex)
1095 MessageBox.Show(ex.Message);
1099 private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1101 var sourceUri = this.CurrentPost?.SourceUri;
1102 if (sourceUri == null)
1107 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1109 catch (Exception ex)
1111 MessageBox.Show(ex.Message);
1117 private async void NameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1119 var screenName = this.CurrentPost?.ScreenName;
1120 if (screenName != null)
1121 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1124 private async void DateTimeLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1126 if (this.CurrentPost?.PostUri is { } postUri)
1127 await MyCommon.OpenInBrowserAsync(this, postUri);
1131 public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1133 /// <summary>ステータスバーに表示するテキスト</summary>
1135 /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1137 public string StatusText { get; }
1139 public TweetDetailsViewStatusChengedEventArgs(string statusText)
1140 => this.StatusText = statusText;