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 public TweenMain Owner { get; set; } = null!;
52 /// <summary>プロフィール画像のキャッシュ</summary>
53 public ImageCache IconCache { get; set; } = null!;
55 /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
56 public bool DumpPostClass { get; set; }
58 /// <summary>現在表示中の発言</summary>
59 public PostClass? CurrentPost { get; private set; }
62 public new bool TabStop
65 set => base.TabStop = value;
68 /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
69 public event EventHandler<TweetDetailsViewStatusChengedEventArgs> StatusChanged;
71 /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
72 private string _postBrowserStatusText = "";
74 public TweetDetailsView()
76 this.InitializeComponent();
82 DateTimeLabel.Text = "";
83 SourceLinkLabel.Text = "";
85 new InternetSecurityManager(PostBrowser);
86 this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
89 public async Task ShowPostDetails(PostClass post)
91 this.CurrentPost = post;
93 var loadTasks = new List<Task>();
95 using (ControlTransaction.Update(this.TableLayoutPanel1))
97 SourceLinkLabel.Text = post.Source;
98 SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
104 nameText = "DM FROM <- ";
106 nameText = "DM TO -> ";
112 nameText += post.ScreenName + "/" + post.Nickname;
113 if (post.RetweetedId != null)
114 nameText += " (RT:" + post.RetweetedBy + ")";
116 NameLabel.Text = nameText;
118 var nameForeColor = SystemColors.ControlText;
119 if (post.IsOwl && (SettingManager.Common.OneWayLove || post.IsDm))
120 nameForeColor = SettingManager.Local.ColorOWL;
121 if (post.RetweetedId != null)
122 nameForeColor = SettingManager.Local.ColorRetweet;
124 nameForeColor = SettingManager.Local.ColorFav;
125 NameLabel.ForeColor = nameForeColor;
127 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
129 DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
132 if (this.DumpPostClass)
134 var sb = new StringBuilder(512);
136 sb.Append("-----Start PostClass Dump<br>");
137 sb.AppendFormat("TextFromApi : {0}<br>", post.TextFromApi);
138 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.TextFromApi);
139 sb.AppendFormat("StatusId : {0}<br>", post.StatusId);
140 //sb.AppendFormat("ImageIndex : {0}<br>", post.ImageIndex);
141 sb.AppendFormat("ImageUrl : {0}<br>", post.ImageUrl);
142 sb.AppendFormat("InReplyToStatusId : {0}<br>", post.InReplyToStatusId);
143 sb.AppendFormat("InReplyToUser : {0}<br>", post.InReplyToUser);
144 sb.AppendFormat("IsDM : {0}<br>", post.IsDm);
145 sb.AppendFormat("IsFav : {0}<br>", post.IsFav);
146 sb.AppendFormat("IsMark : {0}<br>", post.IsMark);
147 sb.AppendFormat("IsMe : {0}<br>", post.IsMe);
148 sb.AppendFormat("IsOwl : {0}<br>", post.IsOwl);
149 sb.AppendFormat("IsProtect : {0}<br>", post.IsProtect);
150 sb.AppendFormat("IsRead : {0}<br>", post.IsRead);
151 sb.AppendFormat("IsReply : {0}<br>", post.IsReply);
153 foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
155 sb.AppendFormat("ReplyToList : {0}<br>", nm);
158 sb.AppendFormat("ScreenName : {0}<br>", post.ScreenName);
159 sb.AppendFormat("NickName : {0}<br>", post.Nickname);
160 sb.AppendFormat("Text : {0}<br>", post.Text);
161 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.Text);
162 sb.AppendFormat("CreatedAt : {0}<br>", post.CreatedAt.ToLocalTimeString());
163 sb.AppendFormat("Source : {0}<br>", post.Source);
164 sb.AppendFormat("UserId : {0}<br>", post.UserId);
165 sb.AppendFormat("FilterHit : {0}<br>", post.FilterHit);
166 sb.AppendFormat("RetweetedBy : {0}<br>", post.RetweetedBy);
167 sb.AppendFormat("RetweetedId : {0}<br>", post.RetweetedId);
169 sb.AppendFormat("Media.Count : {0}<br>", post.Media.Count);
170 if (post.Media.Count > 0)
172 for (var i = 0; i < post.Media.Count; i++)
174 var info = post.Media[i];
175 sb.AppendFormat("Media[{0}].Url : {1}<br>", i, info.Url);
176 sb.AppendFormat("Media[{0}].VideoUrl : {1}<br>", i, info.VideoUrl ?? "---");
179 sb.Append("-----End PostClass Dump<br>");
181 PostBrowser.DocumentText = this.Owner.createDetailHtml(sb.ToString());
185 using (ControlTransaction.Update(this.PostBrowser))
187 this.PostBrowser.DocumentText =
188 this.Owner.createDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
190 this.PostBrowser.Document.Window.ScrollTo(0, 0);
193 loadTasks.Add(this.AppendQuoteTweetAsync(post));
195 await Task.WhenAll(loadTasks);
198 public void ScrollDownPostBrowser(bool forward)
200 var doc = PostBrowser.Document;
201 if (doc == null) return;
203 var tags = doc.GetElementsByTagName("html");
207 tags[0].ScrollTop += SettingManager.Local.FontDetail.Height;
209 tags[0].ScrollTop -= SettingManager.Local.FontDetail.Height;
213 public void PageDownPostBrowser(bool forward)
215 var doc = PostBrowser.Document;
216 if (doc == null) return;
218 var tags = doc.GetElementsByTagName("html");
222 tags[0].ScrollTop += PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
224 tags[0].ScrollTop -= PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
228 public HtmlElement[] GetLinkElements()
230 return this.PostBrowser.Document.Links.Cast<HtmlElement>()
231 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
235 private async Task SetUserPictureAsync(string imageUrl, bool force = false)
237 if (string.IsNullOrEmpty(imageUrl))
240 if (this.IconCache == null)
243 this.ClearUserPicture();
245 await this.UserPicture.SetImageFromTask(async () =>
247 var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
248 .ConfigureAwait(false);
250 return await image.CloneAsync()
251 .ConfigureAwait(false);
256 /// UserPicture.Image に設定されている画像を破棄します。
258 private void ClearUserPicture()
260 if (this.UserPicture.Image != null)
262 var oldImage = this.UserPicture.Image;
263 this.UserPicture.Image = null;
269 /// 発言詳細欄のツイートURLを展開する
271 private async Task AppendQuoteTweetAsync(PostClass post)
273 var quoteStatusIds = post.QuoteStatusIds;
274 if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
278 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
280 var loadingReplyHtml = string.Empty;
281 if (post.InReplyToStatusId != null)
282 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true);
284 var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
286 using (ControlTransaction.Update(this.PostBrowser))
287 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
290 var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
292 if (post.InReplyToStatusId != null)
293 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true));
295 var quoteHtmls = await Task.WhenAll(loadTweetTasks);
297 // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
298 if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
301 body = post.Text + string.Concat(quoteHtmls);
303 using (ControlTransaction.Update(this.PostBrowser))
304 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
307 private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
309 var post = TabInformations.GetInstance()[statusId];
314 post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId)
315 .ConfigureAwait(false);
317 catch (WebApiException ex)
319 return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
323 if (!TabInformations.GetInstance().AddQuoteTweet(post))
324 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
327 return FormatQuoteTweetHtml(post, isReply);
330 internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
332 var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
333 " — " + WebUtility.HtmlEncode(post.Nickname) +
334 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
335 WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
337 return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
340 internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply)
342 var blockClassName = "quote-tweet";
345 blockClassName += " reply";
347 return "<a class=\"quote-tweet-link\" href=\"//opentween/status/" + statusId + "\">" +
348 $"<blockquote class=\"{blockClassName}\">{innerHtml}</blockquote>" +
353 /// 指定されたHTMLからリンクを除去します
355 internal static string StripLinkTagHtml(string html)
356 => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
358 public async Task DoTranslation()
360 if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
363 await this.DoTranslation(this.CurrentPost.TextFromApi);
366 private async Task DoTranslation(string str)
368 if (string.IsNullOrEmpty(str))
371 var bing = new Bing();
374 var translatedText = await bing.TranslateAsync(str,
376 langTo: SettingManager.Common.TranslateLanguage);
378 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(translatedText);
380 catch (HttpRequestException e)
382 this.RaiseStatusChanged("Err:" + e.Message);
384 catch (OperationCanceledException)
386 this.RaiseStatusChanged("Err:Timeout");
390 private async Task DoSearchToolStrip(string url)
392 //発言詳細で「選択文字列で検索」(選択文字列取得)
393 var _selText = this.PostBrowser.GetSelectedText();
395 if (_selText != null)
397 if (url == Properties.Resources.SearchItem4Url)
400 this.Owner.AddNewTabForSearch(_selText);
404 var tmp = string.Format(url, Uri.EscapeDataString(_selText));
405 await this.Owner.OpenUriInBrowserAsync(tmp);
409 private string? GetUserId()
411 var m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
412 if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
413 return m.Result("${ScreenName}");
418 protected void RaiseStatusChanged(string statusText)
419 => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
421 private void TweetDetailsView_FontChanged(object sender, EventArgs e)
423 // OTBaseForm.GlobalFont による UI フォントの変更に対応
424 var origFont = this.NameLabel.Font;
425 this.NameLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
428 #region TableLayoutPanel1
430 private async void UserPicture_DoubleClick(object sender, EventArgs e)
432 if (this.CurrentPost == null)
435 await this.Owner.OpenUriInBrowserAsync(MyCommon.TwitterUrl + this.CurrentPost.ScreenName);
438 private void UserPicture_MouseEnter(object sender, EventArgs e)
439 => this.UserPicture.Cursor = Cursors.Hand;
441 private void UserPicture_MouseLeave(object sender, EventArgs e)
442 => this.UserPicture.Cursor = Cursors.Default;
444 private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
446 if (e.Url.AbsoluteUri != "about:blank")
448 await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
449 await this.Owner.OpenUriInBrowserAsync(e.Url.OriginalString);
453 private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
455 if (e.Url.Scheme == "data")
457 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
459 else if (e.Url.AbsoluteUri != "about:blank")
462 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
463 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
467 private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
469 var KeyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
476 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
478 var shortcut = (Shortcut)e.KeyData;
483 case Shortcut.CtrlIns:
487 // その他のショートカットキーは無効にする
494 if (asyncTask != null)
498 private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
502 if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
503 || PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
504 || PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
506 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
508 if (string.IsNullOrEmpty(PostBrowser.StatusText))
510 this.RaiseStatusChanged(statusText: "");
518 private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
520 var sourceUri = this.CurrentPost?.SourceUri;
521 if (sourceUri != null && e.Button == MouseButtons.Left)
523 await this.Owner.OpenUriInBrowserAsync(sourceUri.AbsoluteUri);
527 private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
529 var sourceUri = this.CurrentPost?.SourceUri;
530 if (sourceUri != null)
532 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
536 private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
537 => this.RaiseStatusChanged(statusText: "");
541 #region ContextMenuUserPicture
543 private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
545 //発言詳細のアイコン右クリック時のメニュー制御
546 if (this.CurrentPost != null)
548 var name = this.CurrentPost.ImageUrl;
549 if (!string.IsNullOrEmpty(name))
551 var idx = name.LastIndexOf('/');
554 name = Path.GetFileName(name.Substring(idx));
555 if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
557 name = name.Replace("_normal", "");
558 this.IconNameToolStripMenuItem.Text = name;
559 this.IconNameToolStripMenuItem.Enabled = true;
563 this.IconNameToolStripMenuItem.Enabled = false;
564 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
569 this.IconNameToolStripMenuItem.Enabled = false;
570 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
573 this.ReloadIconToolStripMenuItem.Enabled = true;
575 if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
577 this.SaveIconPictureToolStripMenuItem.Enabled = true;
581 this.SaveIconPictureToolStripMenuItem.Enabled = false;
586 this.IconNameToolStripMenuItem.Enabled = false;
587 this.ReloadIconToolStripMenuItem.Enabled = false;
588 this.SaveIconPictureToolStripMenuItem.Enabled = false;
589 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
594 this.IconNameToolStripMenuItem.Enabled = false;
595 this.ReloadIconToolStripMenuItem.Enabled = false;
596 this.SaveIconPictureToolStripMenuItem.Enabled = false;
597 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
599 if (this.CurrentPost != null)
601 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
603 FollowToolStripMenuItem.Enabled = false;
604 UnFollowToolStripMenuItem.Enabled = false;
605 ShowFriendShipToolStripMenuItem.Enabled = false;
606 ShowUserStatusToolStripMenuItem.Enabled = true;
607 SearchPostsDetailNameToolStripMenuItem.Enabled = true;
608 SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
609 ListManageUserContextToolStripMenuItem3.Enabled = true;
613 FollowToolStripMenuItem.Enabled = true;
614 UnFollowToolStripMenuItem.Enabled = true;
615 ShowFriendShipToolStripMenuItem.Enabled = true;
616 ShowUserStatusToolStripMenuItem.Enabled = true;
617 SearchPostsDetailNameToolStripMenuItem.Enabled = true;
618 SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
619 ListManageUserContextToolStripMenuItem3.Enabled = true;
624 FollowToolStripMenuItem.Enabled = false;
625 UnFollowToolStripMenuItem.Enabled = false;
626 ShowFriendShipToolStripMenuItem.Enabled = false;
627 ShowUserStatusToolStripMenuItem.Enabled = false;
628 SearchPostsDetailNameToolStripMenuItem.Enabled = false;
629 SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
630 ListManageUserContextToolStripMenuItem3.Enabled = false;
634 private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
636 if (this.CurrentPost == null)
639 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
642 await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
645 private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
647 if (this.CurrentPost == null)
650 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
653 await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
656 private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
658 if (this.CurrentPost == null)
661 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
664 await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
667 // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
669 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
671 if (this.CurrentPost == null)
674 await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
677 private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
679 if (this.CurrentPost == null)
682 await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
685 private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
687 if (this.CurrentPost == null)
690 this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
693 private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
695 var imageUrl = this.CurrentPost?.ImageUrl;
696 if (string.IsNullOrEmpty(imageUrl))
699 await this.Owner.OpenUriInBrowserAsync(imageUrl.Remove(imageUrl.LastIndexOf("_normal", StringComparison.Ordinal), 7)); // "_normal".Length
702 private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
704 var imageUrl = this.CurrentPost?.ImageUrl;
705 if (string.IsNullOrEmpty(imageUrl))
708 await this.SetUserPictureAsync(imageUrl, force: true);
711 private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
713 var imageUrl = this.CurrentPost?.ImageUrl;
714 if (string.IsNullOrEmpty(imageUrl))
717 var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
718 if (memoryImage == null)
721 this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
723 if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
727 using var orgBmp = new Bitmap(memoryImage.Image);
728 using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
730 using (var g = Graphics.FromImage(bmp2))
732 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
733 g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
735 bmp2.Save(this.Owner.SaveFileDialog1.FileName);
739 //処理中にキャッシュアウトする可能性あり
746 #region ContextMenuPostBrowser
748 private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
751 if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
753 this._postBrowserStatusText = PostBrowser.StatusText;
754 var name = GetUserId();
755 UrlCopyContextMenuItem.Enabled = true;
758 FollowContextMenuItem.Enabled = true;
759 RemoveContextMenuItem.Enabled = true;
760 FriendshipContextMenuItem.Enabled = true;
761 ShowUserStatusContextMenuItem.Enabled = true;
762 SearchPostsDetailToolStripMenuItem.Enabled = true;
763 IdFilterAddMenuItem.Enabled = true;
764 ListManageUserContextToolStripMenuItem.Enabled = true;
765 SearchAtPostsDetailToolStripMenuItem.Enabled = true;
769 FollowContextMenuItem.Enabled = false;
770 RemoveContextMenuItem.Enabled = false;
771 FriendshipContextMenuItem.Enabled = false;
772 ShowUserStatusContextMenuItem.Enabled = false;
773 SearchPostsDetailToolStripMenuItem.Enabled = false;
774 IdFilterAddMenuItem.Enabled = false;
775 ListManageUserContextToolStripMenuItem.Enabled = false;
776 SearchAtPostsDetailToolStripMenuItem.Enabled = false;
779 if (Regex.IsMatch(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
780 UseHashtagMenuItem.Enabled = true;
782 UseHashtagMenuItem.Enabled = false;
786 this._postBrowserStatusText = "";
787 UrlCopyContextMenuItem.Enabled = false;
788 FollowContextMenuItem.Enabled = false;
789 RemoveContextMenuItem.Enabled = false;
790 FriendshipContextMenuItem.Enabled = false;
791 ShowUserStatusContextMenuItem.Enabled = false;
792 SearchPostsDetailToolStripMenuItem.Enabled = false;
793 SearchAtPostsDetailToolStripMenuItem.Enabled = false;
794 UseHashtagMenuItem.Enabled = false;
795 IdFilterAddMenuItem.Enabled = false;
796 ListManageUserContextToolStripMenuItem.Enabled = false;
798 // 文字列選択されていないときは選択文字列関係の項目を非表示に
799 var _selText = this.PostBrowser.GetSelectedText();
800 if (_selText == null)
802 SelectionSearchContextMenuItem.Enabled = false;
803 SelectionCopyContextMenuItem.Enabled = false;
804 SelectionTranslationToolStripMenuItem.Enabled = false;
808 SelectionSearchContextMenuItem.Enabled = true;
809 SelectionCopyContextMenuItem.Enabled = true;
810 SelectionTranslationToolStripMenuItem.Enabled = true;
812 //発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
813 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
814 var fAllFlag = false;
815 foreach (Match mu in ma)
817 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
823 this.FriendshipAllMenuItem.Enabled = fAllFlag;
825 if (this.CurrentPost == null)
826 TranslationToolStripMenuItem.Enabled = false;
828 TranslationToolStripMenuItem.Enabled = true;
833 private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
834 => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
836 private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
837 => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
839 private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
840 => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
842 private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
844 //発言詳細の選択文字列で現在のタブを検索
845 var _selText = this.PostBrowser.GetSelectedText();
847 if (_selText != null)
849 var searchOptions = new SearchWordDialog.SearchOptions(
850 SearchWordDialog.SearchType.Timeline,
853 caseSensitive: false,
856 this.Owner.SearchDialog.ResultOptions = searchOptions;
858 this.Owner.DoTabSearch(
860 searchOptions.CaseSensitive,
861 searchOptions.UseRegex,
862 TweenMain.SEARCHTYPE.NextSearch);
866 private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
869 var _selText = this.PostBrowser.GetSelectedText();
872 Clipboard.SetDataObject(_selText, false, 5, 100);
876 MessageBox.Show(ex.Message);
880 private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
884 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
886 if (link.GetAttribute("href") == this._postBrowserStatusText)
888 var linkStr = link.GetAttribute("title");
889 if (string.IsNullOrEmpty(linkStr))
890 linkStr = link.GetAttribute("href");
892 Clipboard.SetDataObject(linkStr, false, 5, 100);
897 Clipboard.SetDataObject(this._postBrowserStatusText, false, 5, 100);
901 MessageBox.Show(ex.Message);
905 private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
906 => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
908 private async void FollowContextMenuItem_Click(object sender, EventArgs e)
910 var name = GetUserId();
912 await this.Owner.FollowCommand(name);
915 private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
917 var name = GetUserId();
919 await this.Owner.RemoveCommand(name, false);
922 private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
924 var name = GetUserId();
926 await this.Owner.ShowFriendship(name);
929 private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
931 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
932 var ids = new List<string>();
933 foreach (Match mu in ma)
935 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
937 ids.Add(mu.Result("${ScreenName}"));
941 await this.Owner.ShowFriendship(ids.ToArray());
944 private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
946 var name = GetUserId();
948 await this.Owner.ShowUserStatus(name);
951 private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
953 var name = GetUserId();
955 await this.Owner.AddNewTabForUserTimeline(name);
958 private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
960 var name = GetUserId();
961 if (name != null) this.Owner.AddNewTabForSearch("@" + name);
964 private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
966 var name = GetUserId();
968 this.Owner.AddFilterRuleByScreenName(name);
971 private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
973 var menuItem = (ToolStripMenuItem)sender;
976 if (menuItem.Owner == this.ContextMenuPostBrowser)
979 if (user == null) return;
981 else if (this.CurrentPost != null)
983 user = this.CurrentPost.ScreenName;
990 this.Owner.ListManageUserContext(user);
993 private void UseHashtagMenuItem_Click(object sender, EventArgs e)
995 var m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
997 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1000 private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1002 var text = this.PostBrowser.GetSelectedText();
1003 await this.DoTranslation(text);
1006 private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1007 => await this.DoTranslation();
1011 #region ContextMenuSource
1013 private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1015 if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1017 SourceCopyMenuItem.Enabled = false;
1018 SourceUrlCopyMenuItem.Enabled = false;
1022 SourceCopyMenuItem.Enabled = true;
1023 SourceUrlCopyMenuItem.Enabled = true;
1027 private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1029 if (this.CurrentPost == null)
1034 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1036 catch (Exception ex)
1038 MessageBox.Show(ex.Message);
1042 private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1044 var sourceUri = this.CurrentPost?.SourceUri;
1045 if (sourceUri == null)
1050 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1052 catch (Exception ex)
1054 MessageBox.Show(ex.Message);
1061 public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1063 /// <summary>ステータスバーに表示するテキスト</summary>
1065 /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1067 public string StatusText { get; }
1069 public TweetDetailsViewStatusChengedEventArgs(string statusText)
1070 => this.StatusText = statusText;