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;
48 public partial class TweetDetailsView : UserControl
50 private TweenMain Owner
51 => this.owner ?? throw this.NotInitializedException();
53 /// <summary>プロフィール画像のキャッシュ</summary>
54 private ImageCache IconCache
55 => this.iconCache ?? throw this.NotInitializedException();
57 /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
58 public bool DumpPostClass { get; set; }
60 /// <summary>現在表示中の発言</summary>
61 public PostClass? CurrentPost { get; private set; }
63 public ThemeManager Theme
65 get => this.themeManager ?? throw this.NotInitializedException();
66 set => this.themeManager = value;
70 public new bool TabStop
73 set => base.TabStop = value;
76 /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
77 public event EventHandler<TweetDetailsViewStatusChengedEventArgs>? StatusChanged;
79 /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
80 private string postBrowserStatusText = "";
82 private TweenMain? owner;
83 private ImageCache? iconCache;
84 private ThemeManager? themeManager;
86 public TweetDetailsView()
88 this.InitializeComponent();
93 this.AuthorNameLinkLabel.Text = "";
94 this.RetweetedByLinkLabel.Text = "";
95 this.DateTimeLabel.Text = "";
96 this.SourceLinkLabel.Text = "";
98 new InternetSecurityManager(this.PostBrowser);
99 this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
102 public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager)
105 this.iconCache = iconCache;
106 this.themeManager = themeManager;
109 private Exception NotInitializedException()
110 => new InvalidOperationException("Cannot call before initialization");
112 public void ClearPostBrowser()
113 => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml("");
115 public async Task ShowPostDetails(PostClass post)
117 this.CurrentPost = post;
119 var loadTasks = new List<Task>();
121 using (ControlTransaction.Update(this.TableLayoutPanel1))
123 this.SourceLinkLabel.Text = post.Source;
124 this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
130 nameText = "DM FROM <- ";
132 nameText = "DM TO -> ";
138 nameText += post.ScreenName + "/" + post.Nickname;
139 this.AuthorNameLinkLabel.Text = nameText;
141 if (post.RetweetedId != null)
143 this.RetweetedByLinkLabel.Visible = true;
144 this.RetweetedByLinkLabel.Text = $"(RT:{post.RetweetedBy})";
148 this.RetweetedByLinkLabel.Visible = false;
149 this.RetweetedByLinkLabel.Text = "";
152 var nameForeColor = SystemColors.ControlText;
153 if (post.IsOwl && (SettingManager.Instance.Common.OneWayLove || post.IsDm))
154 nameForeColor = this.Theme.ColorOWL;
155 if (post.RetweetedId != null)
156 nameForeColor = this.Theme.ColorRetweet;
158 nameForeColor = this.Theme.ColorFav;
160 this.AuthorNameLinkLabel.LinkColor = nameForeColor;
161 this.AuthorNameLinkLabel.ActiveLinkColor = nameForeColor;
162 this.RetweetedByLinkLabel.LinkColor = nameForeColor;
163 this.RetweetedByLinkLabel.ActiveLinkColor = nameForeColor;
165 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
167 this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
170 if (this.DumpPostClass)
172 var sb = new StringBuilder(512);
174 sb.Append("-----Start PostClass Dump<br>");
175 sb.AppendFormat("TextFromApi : {0}<br>", post.TextFromApi);
176 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.TextFromApi);
177 sb.AppendFormat("StatusId : {0}<br>", post.StatusId);
178 sb.AppendFormat("ImageUrl : {0}<br>", post.ImageUrl);
179 sb.AppendFormat("InReplyToStatusId : {0}<br>", post.InReplyToStatusId);
180 sb.AppendFormat("InReplyToUser : {0}<br>", post.InReplyToUser);
181 sb.AppendFormat("IsDM : {0}<br>", post.IsDm);
182 sb.AppendFormat("IsFav : {0}<br>", post.IsFav);
183 sb.AppendFormat("IsMark : {0}<br>", post.IsMark);
184 sb.AppendFormat("IsMe : {0}<br>", post.IsMe);
185 sb.AppendFormat("IsOwl : {0}<br>", post.IsOwl);
186 sb.AppendFormat("IsProtect : {0}<br>", post.IsProtect);
187 sb.AppendFormat("IsRead : {0}<br>", post.IsRead);
188 sb.AppendFormat("IsReply : {0}<br>", post.IsReply);
190 foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
192 sb.AppendFormat("ReplyToList : {0}<br>", nm);
195 sb.AppendFormat("ScreenName : {0}<br>", post.ScreenName);
196 sb.AppendFormat("NickName : {0}<br>", post.Nickname);
197 sb.AppendFormat("Text : {0}<br>", post.Text);
198 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.Text);
199 sb.AppendFormat("CreatedAt : {0}<br>", post.CreatedAt.ToLocalTimeString());
200 sb.AppendFormat("Source : {0}<br>", post.Source);
201 sb.AppendFormat("UserId : {0}<br>", post.UserId);
202 sb.AppendFormat("FilterHit : {0}<br>", post.FilterHit);
203 sb.AppendFormat("RetweetedBy : {0}<br>", post.RetweetedBy);
204 sb.AppendFormat("RetweetedId : {0}<br>", post.RetweetedId);
206 sb.AppendFormat("Media.Count : {0}<br>", post.Media.Count);
207 if (post.Media.Count > 0)
209 for (var i = 0; i < post.Media.Count; i++)
211 var info = post.Media[i];
212 sb.AppendFormat("Media[{0}].Url : {1}<br>", i, info.Url);
213 sb.AppendFormat("Media[{0}].VideoUrl : {1}<br>", i, info.VideoUrl ?? "---");
216 sb.Append("-----End PostClass Dump<br>");
218 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString());
222 using (ControlTransaction.Update(this.PostBrowser))
224 this.PostBrowser.DocumentText =
225 this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
227 this.PostBrowser.Document.Window.ScrollTo(0, 0);
230 loadTasks.Add(this.AppendQuoteTweetAsync(post));
232 await Task.WhenAll(loadTasks);
235 public void ScrollDownPostBrowser(bool forward)
237 var doc = this.PostBrowser.Document;
238 if (doc == null) return;
240 var tags = doc.GetElementsByTagName("html");
244 tags[0].ScrollTop += this.Theme.FontDetail.Height;
246 tags[0].ScrollTop -= this.Theme.FontDetail.Height;
250 public void PageDownPostBrowser(bool forward)
252 var doc = this.PostBrowser.Document;
253 if (doc == null) return;
255 var tags = doc.GetElementsByTagName("html");
259 tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height;
261 tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height;
265 public HtmlElement[] GetLinkElements()
267 return this.PostBrowser.Document.Links.Cast<HtmlElement>()
268 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
272 private async Task SetUserPictureAsync(string normalImageUrl, bool force = false)
274 if (MyCommon.IsNullOrEmpty(normalImageUrl))
277 if (this.IconCache == null)
280 this.ClearUserPicture();
282 var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width);
283 var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize);
284 if (cachedImage != null)
286 // 既にキャッシュされていればそれを表示して終了
287 this.UserPicture.Image = cachedImage.Clone();
291 // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する
292 var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini");
293 if (fallbackImage != null)
294 this.UserPicture.Image = fallbackImage.Clone();
296 await this.UserPicture.SetImageFromTask(
299 var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize);
300 var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
301 .ConfigureAwait(false);
303 return image.Clone();
305 useStatusImage: false
310 /// UserPicture.Image に設定されている画像を破棄します。
312 private void ClearUserPicture()
314 if (this.UserPicture.Image != null)
316 var oldImage = this.UserPicture.Image;
317 this.UserPicture.Image = null;
323 /// 発言詳細欄のツイートURLを展開する
325 private async Task AppendQuoteTweetAsync(PostClass post)
327 var quoteStatusIds = post.QuoteStatusIds;
328 if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
332 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
334 var loadingReplyHtml = string.Empty;
335 if (post.InReplyToStatusId != null)
336 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true);
338 var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
340 using (ControlTransaction.Update(this.PostBrowser))
341 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
344 var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
346 if (post.InReplyToStatusId != null)
347 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true));
349 var quoteHtmls = await Task.WhenAll(loadTweetTasks);
351 // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
352 if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
355 body = post.Text + string.Concat(quoteHtmls);
357 using (ControlTransaction.Update(this.PostBrowser))
358 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
361 private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
363 var post = TabInformations.GetInstance()[statusId];
368 post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId)
369 .ConfigureAwait(false);
371 catch (WebApiException ex)
373 return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
377 if (!TabInformations.GetInstance().AddQuoteTweet(post))
378 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
381 return FormatQuoteTweetHtml(post, isReply);
384 internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
386 var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
387 " — " + WebUtility.HtmlEncode(post.Nickname) +
388 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
389 WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
391 return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
394 internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply)
396 var blockClassName = "quote-tweet";
399 blockClassName += " reply";
401 return "<a class=\"quote-tweet-link\" href=\"//opentween/status/" + statusId + "\">" +
402 $"<blockquote class=\"{blockClassName}\">{innerHtml}</blockquote>" +
407 /// 指定されたHTMLからリンクを除去します
409 internal static string StripLinkTagHtml(string html)
410 => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
412 public async Task DoTranslation()
414 if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
417 await this.DoTranslation(this.CurrentPost.TextFromApi);
420 private async Task DoTranslation(string str)
422 if (MyCommon.IsNullOrEmpty(str))
425 var bing = new Bing();
428 var translatedText = await bing.TranslateAsync(str,
430 langTo: SettingManager.Instance.Common.TranslateLanguage);
432 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText);
434 catch (WebApiException e)
436 this.RaiseStatusChanged("Err:" + e.Message);
438 catch (OperationCanceledException)
440 this.RaiseStatusChanged("Err:Timeout");
444 private async Task DoSearchToolStrip(string url)
446 // 発言詳細で「選択文字列で検索」(選択文字列取得)
447 var selText = this.PostBrowser.GetSelectedText();
451 if (url == Properties.Resources.SearchItem4Url)
454 this.Owner.AddNewTabForSearch(selText);
458 var tmp = string.Format(url, Uri.EscapeDataString(selText));
459 await MyCommon.OpenInBrowserAsync(this, tmp);
463 private string? GetUserId()
465 var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
466 if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
467 return m.Result("${ScreenName}");
472 protected void RaiseStatusChanged(string statusText)
473 => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
475 private void TweetDetailsView_FontChanged(object sender, EventArgs e)
477 // OTBaseForm.GlobalFont による UI フォントの変更に対応
478 var origFont = this.AuthorNameLinkLabel.Font;
479 this.AuthorNameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
480 this.RetweetedByLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
483 #region TableLayoutPanel1
485 private async void UserPicture_Click(object sender, EventArgs e)
487 var screenName = this.CurrentPost?.ScreenName;
488 if (screenName != null)
489 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
492 private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
494 if (e.Url.AbsoluteUri != "about:blank")
496 await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
497 await MyCommon.OpenInBrowserAsync(this, e.Url.OriginalString);
501 private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
503 if (e.Url.Scheme == "data")
505 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
507 else if (e.Url.AbsoluteUri != "about:blank")
510 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
511 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
515 private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
517 var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
524 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
526 var shortcut = (Shortcut)e.KeyData;
531 case Shortcut.CtrlIns:
535 // その他のショートカットキーは無効にする
542 if (asyncTask != null)
546 private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
550 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
551 || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
552 || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
554 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
556 if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText))
558 this.RaiseStatusChanged(statusText: "");
566 private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
568 var sourceUri = this.CurrentPost?.SourceUri;
569 if (sourceUri != null && e.Button == MouseButtons.Left)
571 await MyCommon.OpenInBrowserAsync(this, sourceUri.AbsoluteUri);
575 private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
577 var sourceUri = this.CurrentPost?.SourceUri;
578 if (sourceUri != null)
580 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
584 private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
585 => this.RaiseStatusChanged(statusText: "");
589 #region ContextMenuUserPicture
591 private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
593 // 発言詳細のアイコン右クリック時のメニュー制御
594 if (this.CurrentPost != null)
596 var name = this.CurrentPost.ImageUrl;
597 if (!MyCommon.IsNullOrEmpty(name))
599 var idx = name.LastIndexOf('/');
602 name = Path.GetFileName(name.Substring(idx));
603 if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
605 name = name.Replace("_normal", "");
606 this.IconNameToolStripMenuItem.Text = name;
607 this.IconNameToolStripMenuItem.Enabled = true;
611 this.IconNameToolStripMenuItem.Enabled = false;
612 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
617 this.IconNameToolStripMenuItem.Enabled = false;
618 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
621 this.ReloadIconToolStripMenuItem.Enabled = true;
623 if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
625 this.SaveIconPictureToolStripMenuItem.Enabled = true;
629 this.SaveIconPictureToolStripMenuItem.Enabled = false;
634 this.IconNameToolStripMenuItem.Enabled = false;
635 this.ReloadIconToolStripMenuItem.Enabled = false;
636 this.SaveIconPictureToolStripMenuItem.Enabled = false;
637 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
642 this.IconNameToolStripMenuItem.Enabled = false;
643 this.ReloadIconToolStripMenuItem.Enabled = false;
644 this.SaveIconPictureToolStripMenuItem.Enabled = false;
645 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
647 if (this.CurrentPost != null)
649 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
651 this.FollowToolStripMenuItem.Enabled = false;
652 this.UnFollowToolStripMenuItem.Enabled = false;
653 this.ShowFriendShipToolStripMenuItem.Enabled = false;
654 this.ShowUserStatusToolStripMenuItem.Enabled = true;
655 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
656 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
657 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
661 this.FollowToolStripMenuItem.Enabled = true;
662 this.UnFollowToolStripMenuItem.Enabled = true;
663 this.ShowFriendShipToolStripMenuItem.Enabled = true;
664 this.ShowUserStatusToolStripMenuItem.Enabled = true;
665 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
666 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
667 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
672 this.FollowToolStripMenuItem.Enabled = false;
673 this.UnFollowToolStripMenuItem.Enabled = false;
674 this.ShowFriendShipToolStripMenuItem.Enabled = false;
675 this.ShowUserStatusToolStripMenuItem.Enabled = false;
676 this.SearchPostsDetailNameToolStripMenuItem.Enabled = false;
677 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
678 this.ListManageUserContextToolStripMenuItem3.Enabled = false;
682 private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
684 if (this.CurrentPost == null)
687 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
690 await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
693 private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
695 if (this.CurrentPost == null)
698 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
701 await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
704 private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
706 if (this.CurrentPost == null)
709 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
712 await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
715 // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
717 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
719 if (this.CurrentPost == null)
722 await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
725 private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
727 if (this.CurrentPost == null)
730 await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
733 private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
735 if (this.CurrentPost == null)
738 this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
741 private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
743 var imageNormalUrl = this.CurrentPost?.ImageUrl;
744 if (MyCommon.IsNullOrEmpty(imageNormalUrl))
747 var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original");
748 await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl);
751 private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
753 var imageUrl = this.CurrentPost?.ImageUrl;
754 if (MyCommon.IsNullOrEmpty(imageUrl))
757 await this.SetUserPictureAsync(imageUrl, force: true);
760 private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
762 var imageUrl = this.CurrentPost?.ImageUrl;
763 if (MyCommon.IsNullOrEmpty(imageUrl))
766 var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
767 if (memoryImage == null)
770 this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
772 if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
776 using var orgBmp = new Bitmap(memoryImage.Image);
777 using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
779 using (var g = Graphics.FromImage(bmp2))
781 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
782 g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
784 bmp2.Save(this.Owner.SaveFileDialog1.FileName);
788 // 処理中にキャッシュアウトする可能性あり
795 #region ContextMenuPostBrowser
797 private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
800 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
802 this.postBrowserStatusText = this.PostBrowser.StatusText;
803 var name = this.GetUserId();
804 this.UrlCopyContextMenuItem.Enabled = true;
807 this.FollowContextMenuItem.Enabled = true;
808 this.RemoveContextMenuItem.Enabled = true;
809 this.FriendshipContextMenuItem.Enabled = true;
810 this.ShowUserStatusContextMenuItem.Enabled = true;
811 this.SearchPostsDetailToolStripMenuItem.Enabled = true;
812 this.IdFilterAddMenuItem.Enabled = true;
813 this.ListManageUserContextToolStripMenuItem.Enabled = true;
814 this.SearchAtPostsDetailToolStripMenuItem.Enabled = true;
818 this.FollowContextMenuItem.Enabled = false;
819 this.RemoveContextMenuItem.Enabled = false;
820 this.FriendshipContextMenuItem.Enabled = false;
821 this.ShowUserStatusContextMenuItem.Enabled = false;
822 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
823 this.IdFilterAddMenuItem.Enabled = false;
824 this.ListManageUserContextToolStripMenuItem.Enabled = false;
825 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
828 if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
829 this.UseHashtagMenuItem.Enabled = true;
831 this.UseHashtagMenuItem.Enabled = false;
835 this.postBrowserStatusText = "";
836 this.UrlCopyContextMenuItem.Enabled = false;
837 this.FollowContextMenuItem.Enabled = false;
838 this.RemoveContextMenuItem.Enabled = false;
839 this.FriendshipContextMenuItem.Enabled = false;
840 this.ShowUserStatusContextMenuItem.Enabled = false;
841 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
842 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
843 this.UseHashtagMenuItem.Enabled = false;
844 this.IdFilterAddMenuItem.Enabled = false;
845 this.ListManageUserContextToolStripMenuItem.Enabled = false;
847 // 文字列選択されていないときは選択文字列関係の項目を非表示に
848 var selText = this.PostBrowser.GetSelectedText();
851 this.SelectionSearchContextMenuItem.Enabled = false;
852 this.SelectionCopyContextMenuItem.Enabled = false;
853 this.SelectionTranslationToolStripMenuItem.Enabled = false;
857 this.SelectionSearchContextMenuItem.Enabled = true;
858 this.SelectionCopyContextMenuItem.Enabled = true;
859 this.SelectionTranslationToolStripMenuItem.Enabled = true;
861 // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
862 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
863 var fAllFlag = false;
864 foreach (Match mu in ma)
866 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
872 this.FriendshipAllMenuItem.Enabled = fAllFlag;
874 if (this.CurrentPost == null)
875 this.TranslationToolStripMenuItem.Enabled = false;
877 this.TranslationToolStripMenuItem.Enabled = true;
882 private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
883 => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
885 private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
886 => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
888 private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
889 => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
891 private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
893 // 発言詳細の選択文字列で現在のタブを検索
894 var selText = this.PostBrowser.GetSelectedText();
898 var searchOptions = new SearchWordDialog.SearchOptions(
899 SearchWordDialog.SearchType.Timeline,
902 CaseSensitive: false,
906 this.Owner.SearchDialog.ResultOptions = searchOptions;
908 this.Owner.DoTabSearch(
910 searchOptions.CaseSensitive,
911 searchOptions.UseRegex,
912 TweenMain.SEARCHTYPE.NextSearch);
916 private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
919 var selText = this.PostBrowser.GetSelectedText();
922 Clipboard.SetDataObject(selText, false, 5, 100);
926 MessageBox.Show(ex.Message);
930 private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
934 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
936 if (link.GetAttribute("href") == this.postBrowserStatusText)
938 var linkStr = link.GetAttribute("title");
939 if (MyCommon.IsNullOrEmpty(linkStr))
940 linkStr = link.GetAttribute("href");
942 Clipboard.SetDataObject(linkStr, false, 5, 100);
947 Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100);
951 MessageBox.Show(ex.Message);
955 private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
956 => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
958 private async void FollowContextMenuItem_Click(object sender, EventArgs e)
960 var name = this.GetUserId();
962 await this.Owner.FollowCommand(name);
965 private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
967 var name = this.GetUserId();
969 await this.Owner.RemoveCommand(name, false);
972 private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
974 var name = this.GetUserId();
976 await this.Owner.ShowFriendship(name);
979 private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
981 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
982 var ids = new List<string>();
983 foreach (Match mu in ma)
985 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
987 ids.Add(mu.Result("${ScreenName}"));
991 await this.Owner.ShowFriendship(ids.ToArray());
994 private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
996 var name = this.GetUserId();
998 await this.Owner.ShowUserStatus(name);
1001 private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
1003 var name = this.GetUserId();
1005 await this.Owner.AddNewTabForUserTimeline(name);
1008 private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
1010 var name = this.GetUserId();
1011 if (name != null) this.Owner.AddNewTabForSearch("@" + name);
1014 private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
1016 var name = this.GetUserId();
1018 this.Owner.AddFilterRuleByScreenName(name);
1021 private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
1023 var menuItem = (ToolStripMenuItem)sender;
1026 if (menuItem.Owner == this.ContextMenuPostBrowser)
1028 user = this.GetUserId();
1029 if (user == null) return;
1031 else if (this.CurrentPost != null)
1033 user = this.CurrentPost.ScreenName;
1040 this.Owner.ListManageUserContext(user);
1043 private void UseHashtagMenuItem_Click(object sender, EventArgs e)
1045 var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
1047 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1050 private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1052 var text = this.PostBrowser.GetSelectedText();
1053 await this.DoTranslation(text);
1056 private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1057 => await this.DoTranslation();
1061 #region ContextMenuSource
1063 private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1065 if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1067 this.SourceCopyMenuItem.Enabled = false;
1068 this.SourceUrlCopyMenuItem.Enabled = false;
1072 this.SourceCopyMenuItem.Enabled = true;
1073 this.SourceUrlCopyMenuItem.Enabled = true;
1077 private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1079 if (this.CurrentPost == null)
1084 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1086 catch (Exception ex)
1088 MessageBox.Show(ex.Message);
1092 private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1094 var sourceUri = this.CurrentPost?.SourceUri;
1095 if (sourceUri == null)
1100 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1102 catch (Exception ex)
1104 MessageBox.Show(ex.Message);
1110 private async void AuthorNameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1112 var screenName = this.CurrentPost?.ScreenName;
1113 if (screenName != null)
1114 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1117 private async void RetweetedByLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1119 var screenName = this.CurrentPost?.RetweetedBy;
1120 if (screenName != null)
1121 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1125 public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1127 /// <summary>ステータスバーに表示するテキスト</summary>
1129 /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1131 public string StatusText { get; }
1133 public TweetDetailsViewStatusChengedEventArgs(string statusText)
1134 => this.StatusText = statusText;