OSDN Git Service

画像共有サービスに使用するインタフェースを設計し直し
[opentween/open-tween.git] / OpenTween / Tween.cs
index 794f585..eb7585a 100644 (file)
@@ -211,7 +211,6 @@ namespace OpenTween
 
         private bool _DoFavRetweetFlags = false;
         private bool osResumed = false;
-        private Dictionary<string, IMultimediaShareService> pictureService;
 
         //////////////////////////////////////////////////////////////////////////////////////////////////////////
         private string _postBrowserStatusText = "";
@@ -296,7 +295,7 @@ namespace OpenTween
             public long? inReplyToId = null;
             public string inReplyToName = null;
             public string imageService = "";      //画像投稿サービス名
-            public string imagePath = "";
+            public string[] imagePath = null;
             public PostingStatus()
             {
             }
@@ -846,6 +845,7 @@ namespace OpenTween
             SettingDialog.UseAtIdSupplement = _cfgCommon.UseAtIdSupplement;
             SettingDialog.UseHashSupplement = _cfgCommon.UseHashSupplement;
             SettingDialog.PreviewEnable = _cfgCommon.PreviewEnable;
+            SettingDialog.StatusAreaAtBottom = _cfgCommon.StatusAreaAtBottom;
             AtIdSupl = new AtIdSupplement(SettingAtIdList.Load().AtIdList, "@");
 
             SettingDialog.IsMonospace = _cfgCommon.IsMonospace;
@@ -1015,11 +1015,7 @@ namespace OpenTween
             AllrepliesToolStripMenuItem.Checked = tw.AllAtReply;
 
             //画像投稿サービス
-            this.CreatePictureServices();
-            SetImageServiceCombo();
-            ImageSelectionPanel.Enabled = false;
-
-            SelectImageServiceComboItem(_cfgCommon.UseImageServiceName, _cfgCommon.UseImageService);
+            ImageSelector.Initialize(tw, SettingDialog.TwitterConfiguration, _cfgCommon.UseImageServiceName, _cfgCommon.UseImageService);
 
             //ウィンドウ設定
             this.ClientSize = _cfgLocal.FormSize;
@@ -1069,6 +1065,9 @@ namespace OpenTween
             // NameLabel のフォントを OTBaseForm.GlobalFont に変更
             this.NameLabel.Font = this.ReplaceToGlobalFont(this.NameLabel.Font);
 
+            // 必要であれば、発言一覧と発言詳細部・入力欄の上下を入れ替える
+            SplitContainer1.IsPanelInverted = !SettingDialog.StatusAreaAtBottom;
+
             //全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定)
             if (SettingDialog.UnreadManage == false)
             {
@@ -1279,20 +1278,6 @@ namespace OpenTween
             }
         }
 
-        private void CreatePictureServices()
-        {
-            if (this.pictureService != null) this.pictureService.Clear();
-            this.pictureService = null;
-            this.pictureService = new Dictionary<string, IMultimediaShareService> {
-                {"TwitPic", new TwitPic(tw)},
-                {"img.ly", new imgly(tw)},
-                {"yfrog", new yfrog(tw)},
-                {"Twitter", new TwitterPhoto(tw)},
-                {"ついっぷるフォト", new TwipplePhoto(tw)},
-                {"Imgur", new Imgur(tw)},
-            };
-        }
-
         private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
         {
             string txt;
@@ -1443,7 +1428,6 @@ namespace OpenTween
                 doGetFollowersMenu();
                 GetTimeline(MyCommon.WORKERTYPE.NoRetweetIds, 0, 0, "");
                 GetTimeline(MyCommon.WORKERTYPE.Configuration, 0, 0, "");
-                if (InvokeRequired && !IsDisposed) this.Invoke(new MethodInvoker(this.TrimPostChain));
             }
             if (osResumed)
             {
@@ -1460,7 +1444,6 @@ namespace OpenTween
                     GetTimeline(MyCommon.WORKERTYPE.List, 1, 0, "");
                     doGetFollowersMenu();
                     GetTimeline(MyCommon.WORKERTYPE.Configuration, 0, 0, "");
-                    if (InvokeRequired && !IsDisposed) this.Invoke(new MethodInvoker(this.TrimPostChain));
                 }
             }
         }
@@ -1962,13 +1945,13 @@ namespace OpenTween
 
             if (SettingDialog.UnreadManage) _statuses.SetReadAllTab(true, _curTab.Text, _curItemIndex);
             //キャッシュの書き換え
-            ChangeCacheStyleRead(true, _curItemIndex, _curTab);   //既読へ(フォント、文字色)
+            ChangeCacheStyleRead(true, _curItemIndex);   //既読へ(フォント、文字色)
 
             ColorizeList();
             _colorize = true;
         }
 
-        private void ChangeCacheStyleRead(bool Read, int Index, TabPage Tab)
+        private void ChangeCacheStyleRead(bool Read, int Index)
         {
             //Read:true=既読 false=未読
             //未読管理していなかったら既読として扱う
@@ -1979,18 +1962,15 @@ namespace OpenTween
             ListViewItem itm = null;
             PostClass post = null;
 
-            if (Tab.Equals(this._curTab))
-            {
-                this.TryGetListViewItemCache(Index, out itm, out post);
-            }
+            this.TryGetListViewItemCache(Index, out itm, out post);
 
             if (itm == null || post == null)
             {
-                itm = ((DetailsListView)Tab.Tag).Items[Index];
-                post = _statuses[Tab.Text, Index];
+                itm = ((DetailsListView)_curTab.Tag).Items[Index];
+                post = _statuses[_curTab.Text, Index];
             }
 
-            ChangeItemStyleRead(Read, itm, post, ((DetailsListView)Tab.Tag));
+            ChangeItemStyleRead(Read, itm, post, ((DetailsListView)_curTab.Tag));
         }
 
         private void ChangeItemStyleRead(bool Read, ListViewItem Item, PostClass Post, DetailsListView DList)
@@ -2128,7 +2108,7 @@ namespace OpenTween
         {
             if (StatusText.Text.Trim().Length == 0)
             {
-                if (!ImageSelectionPanel.Enabled)
+                if (!ImageSelector.Enabled)
                 {
                     DoRefresh();
                     return;
@@ -2308,10 +2288,10 @@ namespace OpenTween
 
             args.status.inReplyToId = _reply_to_id;
             args.status.inReplyToName = _reply_to_name;
-            if (ImageSelectionPanel.Visible)
+            if (ImageSelector.Visible)
             {
                 //画像投稿
-                if (!TryGetSelectedMedia(out args.status.imageService, out args.status.imagePath))
+                if (!ImageSelector.TryGetSelectedMedia(out args.status.imageService, out args.status.imagePath))
                     return;
             }
 
@@ -2531,15 +2511,22 @@ namespace OpenTween
 
                 case MyCommon.WORKERTYPE.PostMessage:
                     bw.ReportProgress(200);
-                    if (string.IsNullOrEmpty(args.status.imagePath))
+                    if (args.status.imagePath == null || args.status.imagePath.Length == 0 || string.IsNullOrEmpty(args.status.imagePath[0]))
                     {
                         ret = tw.PostStatus(args.status.status, args.status.inReplyToId);
                     }
                     else
                     {
-                        ret = this.pictureService[args.status.imageService].Upload(ref args.status.imagePath,
-                                                                                   ref args.status.status,
-                                                                                   args.status.inReplyToId);
+                        var service = ImageSelector.GetService(args.status.imageService);
+                        try
+                        {
+                            service.PostStatusAsync(args.status.status, args.status.inReplyToId, args.status.imagePath)
+                                .Wait();
+                        }
+                        catch (AggregateException ex)
+                        {
+                            ret = ex.InnerException.Message;
+                        }
                     }
                     bw.ReportProgress(300);
                     rslt.status = args.status;
@@ -2969,7 +2956,7 @@ namespace OpenTween
                                                 {
                                                     post = _statuses[rslt.sIds[i]];
                                                 }
-                                                ChangeCacheStyleRead(post.IsRead, idx, _curTab);
+                                                ChangeCacheStyleRead(post.IsRead, idx);
                                             }
                                             if (idx == _curItemIndex) DispSelectedPost(true); //選択アイテム再表示
                                         }
@@ -3076,7 +3063,10 @@ namespace OpenTween
                     //_waitFollower = false
                     if (SettingDialog.TwitterConfiguration.PhotoSizeLimit != 0)
                     {
-                        pictureService["Twitter"].Configuration("MaxUploadFilesize", SettingDialog.TwitterConfiguration.PhotoSizeLimit);
+                        foreach (var service in this.ImageSelector.GetServices())
+                        {
+                            service.UpdateTwitterConfiguration(this.SettingDialog.TwitterConfiguration);
+                        }
                     }
                     this.PurgeListViewItemCache();
                     if (_curList != null) _curList.Refresh();
@@ -3108,6 +3098,23 @@ namespace OpenTween
             }
         }
 
+        private async Task RefreshMuteUserIdsAsync()
+        {
+            this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Start;
+
+            try
+            {
+                await tw.RefreshMuteUserIdsAsync();
+            }
+            catch (WebApiException ex)
+            {
+                this.StatusLabel.Text = string.Format(Properties.Resources.UpdateMuteUserIds_Error, ex.Message);
+                return;
+            }
+
+            this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Finish;
+        }
+
         private void RemovePostFromFavTab(Int64[] ids)
         {
             string favTabName = _statuses.GetTabByType(MyCommon.TabUsageType.Favorites).TabName;
@@ -3743,7 +3750,7 @@ namespace OpenTween
                 }
                 foreach (int idx in _curList.SelectedIndices)
                 {
-                    ChangeCacheStyleRead(true, idx, _curTab);
+                    ChangeCacheStyleRead(true, idx);
                 }
                 ColorizeList();
             }
@@ -3773,7 +3780,7 @@ namespace OpenTween
                 }
                 foreach (int idx in _curList.SelectedIndices)
                 {
-                    ChangeCacheStyleRead(false, idx, _curTab);
+                    ChangeCacheStyleRead(false, idx);
                 }
                 ColorizeList();
             }
@@ -3916,7 +3923,8 @@ namespace OpenTween
                                                         SettingDialog.ProxyPort,
                                                         SettingDialog.ProxyUser,
                                                         SettingDialog.ProxyPassword);
-                    this.CreatePictureServices();
+
+                    ImageSelector.Reset(tw, SettingDialog.TwitterConfiguration);
 
                     try
                     {
@@ -3971,6 +3979,8 @@ namespace OpenTween
                     // タブの表示位置の決定
                     SetTabAlignment();
 
+                    SplitContainer1.IsPanelInverted = !SettingDialog.StatusAreaAtBottom;
+
                     var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
                     imgazyobizinet.Enabled = this.SettingDialog.EnableImgAzyobuziNet;
                     imgazyobizinet.DisabledInDM = this.SettingDialog.ImgAzyobuziNetDisabledInDM;
@@ -4117,7 +4127,6 @@ namespace OpenTween
 
                     if (uid != tw.Username) this.doGetFollowersMenu();
 
-                    SetImageServiceCombo();
                     if (SettingDialog.IsNotifyUseGrowl) gh.RegisterGrowl();
                     try
                     {
@@ -4140,7 +4149,27 @@ namespace OpenTween
         /// </summary>
         private void SetTabAlignment()
         {
-            ListTab.Alignment = (SettingDialog.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top);
+            var newAlignment = SettingDialog.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top;
+            if (ListTab.Alignment == newAlignment) return;
+
+            //現在の選択状態を退避
+            Dictionary<string, long[]> selId = new Dictionary<string, long[]>();
+            Dictionary<string, long[]> focusedId = new Dictionary<string, long[]>();
+            SaveSelectedStatus(selId, focusedId);
+
+            ListTab.Alignment = newAlignment;
+
+            //選択状態を復帰
+            foreach (TabPage tab in ListTab.TabPages)
+            {
+                DetailsListView lst = (DetailsListView)tab.Tag;
+                using (ControlTransaction.Update(lst))
+                {
+                    this.SelectListItem(lst,
+                                        _statuses.IndexOf(tab.Text, selId[tab.Text]),
+                                        _statuses.IndexOf(tab.Text, focusedId[tab.Text]));
+                }
+            }
         }
 
         private void ApplyListViewIconSize(MyCommon.IconSizes iconSz)
@@ -5016,7 +5045,7 @@ namespace OpenTween
                 //    pLen += m.Result("${url}").Length - SettingDialog.TwitterConfiguration.ShortUrlLength;
                 //}
             }
-            if (ImageSelectionPanel.Visible && ImageSelectedPicture.Tag != null && !string.IsNullOrEmpty(this.ImageService))
+            if (ImageSelector.Visible && !string.IsNullOrEmpty(ImageSelector.ServiceName))
             {
                 pLen -= SettingDialog.TwitterConfiguration.CharactersReservedPerMedia;
             }
@@ -5238,10 +5267,10 @@ namespace OpenTween
         {
             if (e.State == 0) return;
             e.DrawDefault = false;
+
+            SolidBrush brs2 = null;
             if (!e.Item.Selected)     //e.ItemStateでうまく判定できない???
             {
-                SolidBrush brs2 = null;
-
                 if (e.Item.BackColor == _clSelf)
                     brs2 = _brsBackColorMine;
                 else if (e.Item.BackColor == _clAtSelf)
@@ -5256,18 +5285,17 @@ namespace OpenTween
                     brs2 = _brsBackColorAtTo;
                 else
                     brs2 = _brsBackColorNone;
-
-                e.Graphics.FillRectangle(brs2, e.Bounds);
             }
             else
             {
                 //選択中の行
                 if (((Control)sender).Focused)
-                    e.Graphics.FillRectangle(_brsHighLight, e.Bounds);
+                    brs2 = _brsHighLight;
                 else
-                    e.Graphics.FillRectangle(_brsDeactiveSelection, e.Bounds);
+                    brs2 = _brsDeactiveSelection;
             }
-            if ((e.State & ListViewItemStates.Focused) == ListViewItemStates.Focused) e.DrawFocusRectangle();
+            e.Graphics.FillRectangle(brs2, e.Bounds);
+            e.DrawFocusRectangle();
             this.DrawListViewItemIcon(e);
         }
 
@@ -5280,18 +5308,19 @@ namespace OpenTween
                 //アイコン以外の列
                 RectangleF rct = e.Bounds;
                 rct.Width = e.Header.Width;
+                int fontHeight = e.Item.Font.Height;
                 if (_iconCol)
                 {
-                    rct.Y += e.Item.Font.Height;
-                    rct.Height -= e.Item.Font.Height;
+                    rct.Y += fontHeight;
+                    rct.Height -= fontHeight;
                 }
 
                 int heightDiff;
-                int drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, e.Item.Font.Height, out heightDiff));
+                int drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out heightDiff));
 
-                //if (heightDiff > e.Item.Font.Height * 0.7)
+                //if (heightDiff > fontHeight * 0.7)
                 //{
-                //    rct.Height += e.Item.Font.Height;
+                //    rct.Height += fontHeight;
                 //    drawLineCount += 1;
                 //}
 
@@ -5299,13 +5328,13 @@ namespace OpenTween
                 if (!_iconCol && drawLineCount <= 1)
                 {
                     //rct.Inflate(0, heightDiff / -2);
-                    //rct.Height += e.Item.Font.Height / 2;
+                    //rct.Height += fontHeight / 2;
                 }
-                else if (heightDiff < e.Item.Font.Height * 0.7)
+                else if (heightDiff < fontHeight * 0.7)
                 {
                     //最終行が70%以上欠けていたら、最終行は表示しない
-                    //rct.Height = (float)((e.Item.Font.Height * drawLineCount) + (e.Item.Font.Height / 2));
-                    rct.Height = (e.Item.Font.Height * drawLineCount) - 1;
+                    //rct.Height = (float)((fontHeight * drawLineCount) + (fontHeight / 2));
+                    rct.Height = (fontHeight * drawLineCount) - 1;
                 }
                 else
                 {
@@ -5314,8 +5343,8 @@ namespace OpenTween
 
                 //if (!_iconCol && drawLineCount > 1)
                 //{
-                //    rct.Y += e.Item.Font.Height * 0.2;
-                //    if (heightDiff >= e.Item.Font.Height * 0.8) rct.Height -= e.Item.Font.Height * 0.2;
+                //    rct.Y += fontHeight * 0.2;
+                //    if (heightDiff >= fontHeight * 0.8) rct.Height -= fontHeight * 0.2;
                 //}
 
                 if (rct.Width > 0)
@@ -5326,9 +5355,9 @@ namespace OpenTween
 
                     if (_iconCol)
                     {
-                        RectangleF rctB = e.Bounds;
+                        Rectangle rctB = e.Bounds;
                         rctB.Width = e.Header.Width;
-                        rctB.Height = e.Item.Font.Height;
+                        rctB.Height = fontHeight;
 
                         using (Font fnt = new Font(e.Item.Font, FontStyle.Bold))
                         {
@@ -5344,7 +5373,7 @@ namespace OpenTween
                             TextRenderer.DrawText(e.Graphics,
                                                     e.Item.SubItems[4].Text + " / " + e.Item.SubItems[1].Text + " (" + e.Item.SubItems[3].Text + ") " + e.Item.SubItems[5].Text + e.Item.SubItems[6].Text + " [" + e.Item.SubItems[7].Text + "]",
                                                     fnt,
-                                                    Rectangle.Round(rctB),
+                                                    rctB,
                                                     color,
                                                     TextFormatFlags.SingleLine |
                                                     TextFormatFlags.EndEllipsis |
@@ -5384,16 +5413,22 @@ namespace OpenTween
 
         private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
         {
+            if (_iconSz == 0) return;
+
             ImageListViewItem item = (ImageListViewItem)e.Item;
 
             //e.Bounds.Leftが常に0を指すから自前で計算
             Rectangle itemRect = item.Bounds;
-            itemRect.Width = e.Item.ListView.Columns[0].Width;
+            var col0 = e.Item.ListView.Columns[0];
+            itemRect.Width = col0.Width;
 
-            foreach (ColumnHeader clm in e.Item.ListView.Columns)
+            if (col0.DisplayIndex > 0)
             {
-                if (clm.DisplayIndex < e.Item.ListView.Columns[0].DisplayIndex)
-                    itemRect.X += clm.Width;
+                foreach (ColumnHeader clm in e.Item.ListView.Columns)
+                {
+                    if (clm.DisplayIndex < col0.DisplayIndex)
+                        itemRect.X += clm.Width;
+                }
             }
 
             // ディスプレイの DPI 設定を考慮したアイコンサイズ
@@ -5401,37 +5436,35 @@ namespace OpenTween
             var realStateSize = new SizeF(16 * this.currentScaleFactor.Width, 16 * this.currentScaleFactor.Height).ToSize();
 
             Rectangle iconRect;
-            Rectangle stateRect;
-            if (item.Image != null)
+            var img = item.Image;
+            if (img != null)
             {
                 iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, realIconSize), itemRect);
                 iconRect.Offset(0, Math.Max(0, (itemRect.Height - realIconSize.Height) / 2));
-                stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect);
+
+                if (iconRect.Width > 0)
+                {
+                    e.Graphics.FillRectangle(Brushes.White, iconRect);
+                    e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
+                    try
+                    {
+                        e.Graphics.DrawImage(img.Image, iconRect);
+                    }
+                    catch (ArgumentException)
+                    {
+                        item.RefreshImageAsync();
+                    }
+                }
             }
             else
             {
                 iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, new Size(1, 1)), itemRect);
                 //iconRect.Offset(0, Math.Max(0, (itemRect.Height - realIconSize.Height) / 2));
-                stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect);
-            }
-
-            var img = item.Image;
-            if (img != null && iconRect.Width > 0)
-            {
-                e.Graphics.FillRectangle(Brushes.White, iconRect);
-                e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
-                try
-                {
-                    e.Graphics.DrawImage(img.Image, iconRect);
-                }
-                catch (ArgumentException)
-                {
-                    item.RefreshImageAsync();
-                }
             }
 
             if (item.StateImageIndex > -1)
             {
+                Rectangle stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect);
                 if (stateRect.Width > 0)
                 {
                     //e.Graphics.FillRectangle(Brushes.White, stateRect);
@@ -5757,7 +5790,7 @@ namespace OpenTween
             int idx = -1;
             DetailsListView lst = null;
 
-            if (ImageSelectionPanel.Enabled)
+            if (ImageSelector.Enabled)
                 return;
 
             //現在タブから最終タブまで探索
@@ -6170,7 +6203,10 @@ namespace OpenTween
                             if (this.thumbnailTokenSource != null)
                             {
                                 var oldTokenSource = this.thumbnailTokenSource;
-                                oldTokenSource.Cancel();
+
+                                // TODO: キャンセルを行うとUIスレッドが阻害される問題を調査
+                                //oldTokenSource.Cancel();
+
                                 this.thumbnailTask.ContinueWith(_ => oldTokenSource.Dispose());
                             }
 
@@ -7535,35 +7571,78 @@ namespace OpenTween
 
         private void GoBackSelectPostChain()
         {
-            try
+            if (this.selectPostChains.Count > 1)
             {
-                this.selectPostChains.Pop();
-                Tuple<TabPage, PostClass> tabPostPair = this.selectPostChains.Pop();
-                if (!this.ListTab.TabPages.Contains(tabPostPair.Item1)) return;
-                this.ListTab.SelectedTab = tabPostPair.Item1;
-                if (tabPostPair.Item2 != null && this._statuses.Tabs[this._curTab.Text].IndexOf(tabPostPair.Item2.StatusId) > -1)
+                var idx = -1;
+                TabPage tp = null;
+
+                do
                 {
-                    this.SelectListItem(this._curList, this._statuses.Tabs[this._curTab.Text].IndexOf(tabPostPair.Item2.StatusId));
-                    this._curList.EnsureVisible(this._statuses.Tabs[this._curTab.Text].IndexOf(tabPostPair.Item2.StatusId));
+                    try
+                    {
+                        this.selectPostChains.Pop();
+                        var tabPostPair = this.selectPostChains.Peek();
+
+                        if (!this.ListTab.TabPages.Contains(tabPostPair.Item1)) continue;  //該当タブが存在しないので無視
+
+                        if (tabPostPair.Item2 != null)
+                        {
+                            idx = this._statuses.Tabs[tabPostPair.Item1.Text].IndexOf(tabPostPair.Item2.StatusId);
+                            if (idx == -1) continue;  //該当ポストが存在しないので無視
+                        }
+
+                        tp = tabPostPair.Item1;
+
+                        this.selectPostChains.Pop();
+                    }
+                    catch (InvalidOperationException)
+                    {
+                    }
+
+                    break;
                 }
-            }
-            catch (InvalidOperationException)
-            {
+                while (this.selectPostChains.Count > 1);
+
+                if (tp == null)
+                {
+                    //状態がおかしいので処理を中断
+                    //履歴が残り1つであればクリアしておく
+                    if (this.selectPostChains.Count == 1)
+                        this.selectPostChains.Clear();
+                    return;
+                }
+
+                DetailsListView lst = (DetailsListView)tp.Tag;
+                this.ListTab.SelectedTab = tp;
+                if (idx > -1)
+                {
+                    SelectListItem(lst, idx);
+                    lst.EnsureVisible(idx);
+                }
+                lst.Focus();
             }
         }
 
         private void PushSelectPostChain()
         {
-            if (this.selectPostChains.Count == 0 || (this.selectPostChains.Peek().Item1.Text != this._curTab.Text || this._curPost != this.selectPostChains.Peek().Item2))
+            int count = this.selectPostChains.Count;
+            if (count > 0)
             {
-                this.selectPostChains.Push(Tuple.Create(this._curTab, _curPost));
+                var p = this.selectPostChains.Peek();
+                if (p.Item1 == this._curTab)
+                {
+                    if (p.Item2 == this._curPost) return;  //最新の履歴と同一
+                    if (p.Item2 == null) this.selectPostChains.Pop();  //置き換えるため削除
+                }
             }
+            if (count >= 2500) TrimPostChain();
+            this.selectPostChains.Push(Tuple.Create(this._curTab, this._curPost));
         }
 
         private void TrimPostChain()
         {
-            if (this.selectPostChains.Count < 2000) return;
-            Stack<Tuple<TabPage, PostClass>> p = new Stack<Tuple<TabPage, PostClass>>();
+            if (this.selectPostChains.Count <= 2000) return;
+            var p = new Stack<Tuple<TabPage, PostClass>>(2000);
             for (int i = 0; i < 2000; i++)
             {
                 p.Push(this.selectPostChains.Pop());
@@ -7750,6 +7829,7 @@ namespace OpenTween
                 _cfgCommon.UseAtIdSupplement = SettingDialog.UseAtIdSupplement;
                 _cfgCommon.UseHashSupplement = SettingDialog.UseHashSupplement;
                 _cfgCommon.PreviewEnable = SettingDialog.PreviewEnable;
+                _cfgCommon.StatusAreaAtBottom = SettingDialog.StatusAreaAtBottom;
                 _cfgCommon.Language = SettingDialog.Language;
 
                 _cfgCommon.SortOrder = (int)_statuses.SortOrder;
@@ -7806,8 +7886,8 @@ namespace OpenTween
                 _cfgCommon.AllAtReply = tw.AllAtReply;
                 _cfgCommon.OpenUserTimeline = SettingDialog.OpenUserTimeline;
                 _cfgCommon.ListCountApi = SettingDialog.ListCountApi;
-                _cfgCommon.UseImageService = ImageServiceCombo.SelectedIndex;
-                _cfgCommon.UseImageServiceName = this.ImageService;
+                _cfgCommon.UseImageService = ImageSelector.ServiceIndex;
+                _cfgCommon.UseImageServiceName = ImageSelector.ServiceName;
                 _cfgCommon.ListDoubleClickAction = SettingDialog.ListDoubleClickAction;
                 _cfgCommon.UserAppointUrl = SettingDialog.UserAppointUrl;
                 _cfgCommon.HideDuplicatedRetweets = SettingDialog.HideDuplicatedRetweets;
@@ -7919,7 +7999,7 @@ namespace OpenTween
                 }
                 catch (WebApiException ex)
                 {
-                    var message = ex.InnerException.Message;
+                    var message = ex.Message;
                     MessageBox.Show(this, string.Format(Properties.Resources.OpenURL_LoadFailed, message),
                         Properties.Resources.OpenURL_Caption, MessageBoxButtons.OK, MessageBoxIcon.Error);
                     return;
@@ -9722,25 +9802,16 @@ namespace OpenTween
         {
             if (this._curPost == null) return;
 
-            var imageUrl = this._curPost.ImageUrl;
-            try
+            await this.UserPicture.SetImageFromTask(async () =>
             {
-                var image = await this.IconCache.DownloadImageAsync(imageUrl, force: true);
+                var imageUrl = this._curPost.ImageUrl;
 
-                this.ClearUserPicture();
-                this.UserPicture.Image = image.Clone();
-            }
-            catch (Exception)
-            {
-                this.UserPicture.ShowErrorImage();
-                try
-                {
-                    throw;
-                }
-                catch (HttpRequestException) { }
-                catch (InvalidImageException) { }
-                catch (TaskCanceledException) { }
-            }
+                var image = await this.IconCache.DownloadImageAsync(imageUrl, force: true)
+                    .ConfigureAwait(false);
+
+                return await image.CloneAsync()
+                    .ConfigureAwait(false);
+            });
         }
 
         private void SaveOriginalSizeIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
@@ -10761,6 +10832,7 @@ namespace OpenTween
 
             if (this.IsNetworkAvailable())
             {
+                this.RefreshMuteUserIdsAsync();
                 GetTimeline(MyCommon.WORKERTYPE.BlockIds, 0, 0, "");
                 GetTimeline(MyCommon.WORKERTYPE.NoRetweetIds, 0, 0, "");
                 if (SettingDialog.StartupFollowers)
@@ -11750,7 +11822,7 @@ namespace OpenTween
             Match m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
             if (m.Success)
             {
-                HashMgr.SetPermanentHash("#" + m.Result("${hash}"));
+                HashMgr.SetPermanentHash("#" + Uri.UnescapeDataString(m.Result("${hash}")));
                 HashStripSplitButton.Text = HashMgr.UseHash;
                 HashToggleMenuItem.Checked = true;
                 HashToggleToolStripMenuItem.Checked = true;
@@ -12016,7 +12088,7 @@ namespace OpenTween
         {
             public Twitter tw;
             public string id;
-            public TwitterDataModel.User user;
+            public TwitterUser user;
         }
 
         private void GetUserInfo_DoWork(object sender, DoWorkEventArgs e)
@@ -12027,7 +12099,7 @@ namespace OpenTween
 
         private void doShowUserStatus(string id, bool ShowInputDialog)
         {
-            TwitterDataModel.User user = null;
+            TwitterUser user = null;
             GetUserInfoArgs args = new GetUserInfoArgs();
             if (ShowInputDialog)
             {
@@ -12082,15 +12154,18 @@ namespace OpenTween
             }
         }
 
-        private void doShowUserStatus(TwitterDataModel.User user)
+        private async void doShowUserStatus(TwitterUser user)
         {
-            using (ShowUserInfo userinfo = new ShowUserInfo())
+            using (var userDialog = new UserInfoDialog(this, this.tw))
             {
-                userinfo.Owner = this;
-                userinfo.User = user;
-                userinfo.ShowDialog(this);
+                var showUserTask = userDialog.ShowUserAsync(user);
+                userDialog.ShowDialog(this);
+
                 this.Activate();
                 this.BringToFront();
+
+                // ユーザー情報の表示が完了するまで userDialog を破棄しない
+                await showUserTask;
             }
         }
 
@@ -12240,6 +12315,10 @@ namespace OpenTween
             this._apiGauge.BorderSides = ToolStripStatusLabelBorderSides.Right;
             this.StatusStrip1.Items.Insert(2, this._apiGauge);
 
+            this.ImageSelector.Visible = false;
+            this.ImageSelector.Enabled = false;
+            this.ImageSelector.FilePickDialog = OpenFileDialog1;
+
             this.ReplaceAppName();
         }
 
@@ -12296,60 +12375,19 @@ namespace OpenTween
 #region "画像投稿"
         private void ImageSelectMenuItem_Click(object sender, EventArgs e)
         {
-            if (ImageSelectionPanel.Visible)
-            {
-                CloseImageSelectionPanel();
-            }
+            if (ImageSelector.Visible)
+                ImageSelector.EndSelection();
             else
-            {
-                OpenImageSelectionPanel();
-                ImageFromSelectedFile(true);
-                ImagefilePathText.Focus();
-            }
-        }
-
-        private void OpenImageSelectionPanel()
-        {
-            ImageSelectionPanel.Visible = true;
-            ImageSelectionPanel.Enabled = true;
-            TimelinePanel.Visible = false;
-            TimelinePanel.Enabled = false;
-        }
-
-        private void CloseImageSelectionPanel()
-        {
-            ImagefilePathText.CausesValidation = false;
-            TimelinePanel.Visible = true;
-            TimelinePanel.Enabled = true;
-            ImageSelectionPanel.Visible = false;
-            ImageSelectionPanel.Enabled = false;
-            ((DetailsListView)ListTab.SelectedTab.Tag).Focus();
-            ClearImageSelectedPicture(false);
-            ImagefilePathText.CausesValidation = true;
+                ImageSelector.BeginSelection();
         }
 
         private void SelectMedia_DragEnter(DragEventArgs e)
         {
-            string filename = ((string[])e.Data.GetData(DataFormats.FileDrop, false))[0];
-            FileInfo fl = new FileInfo(filename);
-            string ext = fl.Extension;
-
-            if (!string.IsNullOrEmpty(this.ImageService) &&
-                this.pictureService[this.ImageService].CheckValidFilesize(ext, fl.Length))
+            if (ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0]))
             {
                 e.Effect = DragDropEffects.Copy;
                 return;
             }
-            foreach (string svc in ImageServiceCombo.Items)
-            {
-                if (!string.IsNullOrEmpty(svc) &&
-                    this.pictureService[svc].CheckValidFilesize(ext, fl.Length))
-                {
-                    //ImageServiceCombo.SelectedItem = svc;
-                    e.Effect = DragDropEffects.Copy;
-                    return;
-                }
-            }
             e.Effect = DragDropEffects.None;
         }
 
@@ -12362,308 +12400,46 @@ namespace OpenTween
         {
             this.Activate();
             this.BringToFront();
-            OpenImageSelectionPanel();
-            ImagefilePathText.Text = ((string[])e.Data.GetData(DataFormats.FileDrop, false))[0];
-            ImageFromSelectedFile(false);
+            ImageSelector.BeginSelection(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0]);
             StatusText.Focus();
         }
 
-        private void FilePickButton_Click(object sender, EventArgs e)
-        {
-            if (string.IsNullOrEmpty(this.ImageService)) return;
-            OpenFileDialog1.Filter = this.pictureService[this.ImageService].GetFileOpenDialogFilter();
-            OpenFileDialog1.Title = Properties.Resources.PickPictureDialog1;
-            OpenFileDialog1.FileName = "";
-
-            try
-            {
-                this.AllowDrop = false;
-                if (OpenFileDialog1.ShowDialog() == DialogResult.Cancel) return;
-            }
-            finally
-            {
-                this.AllowDrop = true;
-            }
-
-            ImagefilePathText.Text = OpenFileDialog1.FileName;
-            ImageFromSelectedFile(false);
-        }
-
-        private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
-        {
-            if (ImageCancelButton.Focused)
-            {
-                ImagefilePathText.CausesValidation = false;
-                return;
-            }
-
-            ImageFromSelectedFile(false);
-        }
-
-        private void ImageFromSelectedFile(bool suppressMsgBox)
-        {
-            try
-            {
-                ClearImageSelectedPicture(true);
-
-                ImagefilePathText.Text = ImagefilePathText.Text.Trim();
-                if (string.IsNullOrEmpty(ImagefilePathText.Text) || string.IsNullOrEmpty(this.ImageService))
-                {
-                    ImagefilePathText.Text = "";
-                    return;
-                }
-
-                FileInfo fl = new FileInfo(ImagefilePathText.Text);
-                string ext = fl.Extension;
-                var imageService = this.pictureService[this.ImageService];
-
-                if (!imageService.CheckValidExtension(ext))
-                {
-                    //画像以外の形式
-                    ImagefilePathText.Text = "";
-                    if (!suppressMsgBox)
-                    {
-                        MessageBox.Show(
-                            string.Format(Properties.Resources.PostPictureWarn3, this.ImageService, MakeAvailableServiceText(ext, fl.Length), ext),
-                            Properties.Resources.PostPictureWarn4,
-                            MessageBoxButtons.OK,
-                            MessageBoxIcon.Warning);
-                    }
-                    return;
-                }
-
-                if (!imageService.CheckValidFilesize(ext, fl.Length))
-                {
-                    // ファイルサイズが大きすぎる
-                    ImagefilePathText.Text = "";
-                    if (!suppressMsgBox)
-                    {
-                        MessageBox.Show(
-                            string.Format(Properties.Resources.PostPictureWarn5, this.ImageService, MakeAvailableServiceText(ext, fl.Length)),
-                            Properties.Resources.PostPictureWarn4,
-                            MessageBoxButtons.OK,
-                            MessageBoxIcon.Warning);
-                    }
-                    return;
-                }
-
-                switch (imageService.GetFileType(ext))
-                {
-                    case MyCommon.UploadFileType.Invalid:
-                        ImagefilePathText.Text = "";
-                        break;
-                    case MyCommon.UploadFileType.Picture:
-                        Image img = null;
-                        using (FileStream fs = new FileStream(ImagefilePathText.Text, FileMode.Open, FileAccess.Read))
-                        {
-                            img = Image.FromStream(fs);
-                        }
-                        ImageSelectedPicture.Image = (new HttpVarious()).CheckValidImage(
-                                    img,
-                                    img.Width,
-                                    img.Height);
-                        ImageSelectedPicture.Tag = MyCommon.UploadFileType.Picture;
-                        break;
-                    case MyCommon.UploadFileType.MultiMedia:
-                        ImageSelectedPicture.Image = Properties.Resources.MultiMediaImage;
-                        ImageSelectedPicture.Tag = MyCommon.UploadFileType.MultiMedia;
-                        break;
-                    default:
-                        ImagefilePathText.Text = "";
-                        break;
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                ClearImageSelectedPicture(true);
-                ImagefilePathText.Text = "";
-                if (!suppressMsgBox) MessageBox.Show("File not found.");
-            }
-            catch (Exception)
-            {
-                ClearImageSelectedPicture(true);
-                ImagefilePathText.Text = "";
-                if (!suppressMsgBox) MessageBox.Show("The type of this file is not image.");
-            }
-        }
-
-        private string MakeAvailableServiceText(string ext, long fileSize)
+        private void ImageSelector_BeginSelecting(object sender, EventArgs e)
         {
-            StringBuilder sb = new StringBuilder();
-
-            foreach (string svc in ImageServiceCombo.Items)
-            {
-                if (!string.IsNullOrEmpty(svc) &&
-                    this.pictureService[svc].CheckValidFilesize(ext, fileSize))
-                {
-                    if (sb.Length > 0) sb.Append(", ");
-                    sb.Append(svc);
-                }
-            }
-            if (sb.Length == 0)
-                return Properties.Resources.PostPictureWarn6;
-
-            return sb.ToString();
-        }
-
-        private void ClearImageSelectedPicture(bool invalidate)
-        {
-            Image oldImage = null;
-            if (this.ImageSelectedPicture.Image != null)
-            {
-                if (this.ImageSelectedPicture.Image != ImageSelectedPicture.InitialImage &&
-                    this.ImageSelectedPicture.Image != Properties.Resources.MultiMediaImage)
-                {
-                    oldImage = this.ImageSelectedPicture.Image;
-                }
-            }
-            if (invalidate)
-            {
-                this.ImageSelectedPicture.Image = ImageSelectedPicture.InitialImage;
-                this.ImageSelectedPicture.Tag = MyCommon.UploadFileType.Invalid;
-            }
-            else
-            {
-                this.ImageSelectedPicture.Image = null;
-                this.ImageSelectedPicture.Tag = null;
-            }
-            if (oldImage != null) oldImage.Dispose();
-        }
-
-        private void ImageCancelButton_Click(object sender, EventArgs e)
-        {
-            CloseImageSelectionPanel();
-        }
-
-        private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
-        {
-            if (e.KeyCode == Keys.Escape)
-            {
-                CloseImageSelectionPanel();
-            }
-        }
-
-        private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
-        {
-            if (Convert.ToInt32(e.KeyChar) == 0x1B)
-            {
-                ImagefilePathText.CausesValidation = false;
-                e.Handled = true;
-            }
-        }
-
-        private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
-        {
-            if (e.KeyCode == Keys.Escape)
-            {
-                ImagefilePathText.CausesValidation = false;
-            }
-        }
-
-        private bool TryGetSelectedMedia(out string imageService, out string imagePath)
-        {
-            if (ImageSelectedPicture.Image != ImageSelectedPicture.InitialImage &&
-                ImageServiceCombo.SelectedIndex > -1 &&
-                !string.IsNullOrEmpty(ImagefilePathText.Text))
-            {
-                if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, this.ImageService),
-                                   Properties.Resources.PostPictureConfirm2,
-                                   MessageBoxButtons.OKCancel,
-                                   MessageBoxIcon.Question,
-                                   MessageBoxDefaultButton.Button1)
-                               == DialogResult.OK)
-                {
-                    imageService = ImageServiceCombo.Text;
-                    imagePath = ImagefilePathText.Text;
-                    CloseImageSelectionPanel();
-                    ImagefilePathText.Text = "";
-                    return true;
-                }
-            }
-            else
-            {
-                MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
-            }
-
-            CloseImageSelectionPanel();
-            imageService = null;
-            imagePath = null;
-            return false;
+            TimelinePanel.Visible = false;
+            TimelinePanel.Enabled = false;
         }
 
-        private void SetImageServiceCombo()
+        private void ImageSelector_EndSelecting(object sender, EventArgs e)
         {
-            string svc = "";
-            if (ImageServiceCombo.SelectedIndex > -1) svc = ImageServiceCombo.SelectedItem.ToString();
-            ImageServiceCombo.Items.Clear();
-            
-            // Add service names to combobox
-            foreach (var key in pictureService.Keys)
-            {
-                ImageServiceCombo.Items.Add(key);
-            }
-
-            SelectImageServiceComboItem(svc);
+            TimelinePanel.Visible = true;
+            TimelinePanel.Enabled = true;
+            ((DetailsListView)ListTab.SelectedTab.Tag).Focus();
         }
 
-        private void SelectImageServiceComboItem(string svc, int? index = null)
+        private void ImageSelector_FilePickDialogOpening(object sender, EventArgs e)
         {
-            int idx;
-            if (string.IsNullOrEmpty(svc))
-            {
-                idx = index ?? 0;
-            }
-            else
-            {
-                idx = ImageServiceCombo.Items.IndexOf(svc);
-                if (idx == -1) idx = index ?? 0;
-            }
-
-            try
-            {
-                ImageServiceCombo.SelectedIndex = idx;
-            }
-            catch (ArgumentOutOfRangeException)
-            {
-                ImageServiceCombo.SelectedIndex = 0;
-            }
+            this.AllowDrop = false;
         }
 
-        private string ImageService
+        private void ImageSelector_FilePickDialogClosed(object sender, EventArgs e)
         {
-            get { return ImageServiceCombo.SelectedItem.ToString(); }
+            this.AllowDrop = true;
         }
 
-        private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
+        private void ImageSelector_SelectedServiceChanged(object sender, EventArgs e)
         {
-            if (ImageSelectedPicture.Tag != null && !string.IsNullOrEmpty(this.ImageService))
+            if (ImageSelector.Visible)
             {
-                try
-                {
-                    FileInfo fi = new FileInfo(ImagefilePathText.Text.Trim());
-                    string ext = fi.Extension;
-                    var imageService = this.pictureService[this.ImageService];
-                    if (!imageService.CheckValidFilesize(ext, fi.Length))
-                    {
-                        ClearImageSelectedPicture(true);
-                        ImagefilePathText.Text = "";
-                    }
-                }
-                catch (Exception)
-                {
-                    ClearImageSelectedPicture(true);
-                    ImagefilePathText.Text = "";
-                }
                 _modifySettingCommon = true;
                 SaveConfigsAll(true);
-                if (this.ImageService == "Twitter")
-                {
+
+                if (ImageSelector.ServiceName.Equals("Twitter"))
                     this.StatusText_TextChanged(null, null);
-                }
             }
         }
 
-        private void ImageSelectionPanel_VisibleChanged(object sender, EventArgs e)
+        private void ImageSelector_VisibleChanged(object sender, EventArgs e)
         {
             this.StatusText_TextChanged(null, null);
         }