OSDN Git Service

string.ToLowerの使用を避ける (CA1308)
[opentween/open-tween.git] / OpenTween / Twitter.cs
index 132228c..83d4d04 100644 (file)
@@ -50,6 +50,8 @@ using OpenTween.Api;
 using OpenTween.Api.DataModel;
 using OpenTween.Connection;
 using OpenTween.Models;
+using System.Drawing.Imaging;
+using OpenTween.Setting;
 
 namespace OpenTween
 {
@@ -131,6 +133,15 @@ namespace OpenTween
         public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
 
         /// <summary>
+        /// attachment_url に指定可能な URL を判定する正規表現
+        /// </summary>
+        public static readonly Regex AttachmentUrlRegex = new Regex(@"https?://(
+   twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
+ | mobile\.twitter\.com/[0-9A-Za-z_]+/status/[0-9]+
+ | twitter\.com/messages/compose\?recipient_id=[0-9]+(&.+)?
+)$", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+
+        /// <summary>
         /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
         /// </summary>
         public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
@@ -147,18 +158,18 @@ namespace OpenTween
 
         public TwitterApi Api { get; }
         public TwitterConfiguration Configuration { get; private set; }
+        public TwitterTextConfiguration TextConfiguration { get; private set; }
+
+        public bool GetFollowersSuccess { get; private set; } = false;
+        public bool GetNoRetweetSuccess { get; private set; } = false;
 
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
         private ISet<long> followerId = new HashSet<long>();
-        private bool _GetFollowerResult = false;
         private long[] noRTId = new long[0];
-        private bool _GetNoRetweetResult = false;
 
         //プロパティからアクセスされる共通情報
         private string _uname;
-
-        private bool _readOwnPost;
         private List<string> _hashList = new List<string>();
 
         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
@@ -179,20 +190,14 @@ namespace OpenTween
         {
             this.Api = api;
             this.Configuration = TwitterConfiguration.DefaultConfiguration();
+            this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration();
         }
 
         public TwitterApiAccessLevel AccessLevel
-        {
-            get
-            {
-                return MyCommon.TwitterApiInfo.AccessLevel;
-            }
-        }
+            => MyCommon.TwitterApiInfo.AccessLevel;
 
         protected void ResetApiStatus()
-        {
-            MyCommon.TwitterApiInfo.Reset();
-        }
+            => MyCommon.TwitterApiInfo.Reset();
 
         public void ClearAuthInfo()
         {
@@ -230,11 +235,11 @@ namespace OpenTween
             }
             this.ResetApiStatus();
             this.Api.Initialize(token, tokenSecret, userId, username);
-            _uname = username.ToLowerInvariant();
-            if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
+            _uname = username;
+            if (SettingManager.Common.UserstreamStartup) this.ReconnectUserStream();
         }
 
-        public string PreProcessUrl(string orgData)
+        internal static string PreProcessUrl(string orgData)
         {
             int posl1;
             var posl2 = 0;
@@ -274,19 +279,19 @@ namespace OpenTween
             return orgData;
         }
 
-        public async Task PostStatus(string postStr, long? reply_to, IReadOnlyList<long> mediaIds = null)
+        public async Task PostStatus(PostStatusParams param)
         {
             this.CheckAccountState();
 
-            if (mediaIds == null &&
-                Twitter.DMSendTextRegex.IsMatch(postStr))
+            if (Twitter.DMSendTextRegex.IsMatch(param.Text))
             {
-                await this.SendDirectMessage(postStr)
+                await this.SendDirectMessage(param.Text)
                     .ConfigureAwait(false);
                 return;
             }
 
-            var response = await this.Api.StatusesUpdate(postStr, reply_to, mediaIds)
+            var response = await this.Api.StatusesUpdate(param.Text, param.InReplyToStatusId, param.MediaIds,
+                    param.AutoPopulateReplyMetadata, param.ExcludeReplyUserIds, param.AttachmentUrl)
                 .ConfigureAwait(false);
 
             var status = await response.LoadJsonAsync()
@@ -300,12 +305,31 @@ namespace OpenTween
             this.previousStatusId = status.Id;
         }
 
-        public async Task<long> UploadMedia(IMediaItem item)
+        public Task<long> UploadMedia(IMediaItem item)
+            => this.UploadMedia(item, SettingManager.Common.AlphaPNGWorkaround);
+
+        public async Task<long> UploadMedia(IMediaItem item, bool alphaPNGWorkaround)
         {
             this.CheckAccountState();
 
-            var response = await this.Api.MediaUpload(item)
-                .ConfigureAwait(false);
+            LazyJson<TwitterUploadMediaResult> response;
+
+            using (var origImage = item.CreateImage())
+            {
+                if (alphaPNGWorkaround && this.AddAlphaChannelIfNeeded(origImage.Image, out var newImage))
+                {
+                    using (var newMediaItem = new MemoryImageMediaItem(newImage))
+                    {
+                        response = await this.Api.MediaUpload(newMediaItem)
+                            .ConfigureAwait(false);
+                    }
+                }
+                else
+                {
+                    response = await this.Api.MediaUpload(item)
+                        .ConfigureAwait(false);
+                }
+            }
 
             var media = await response.LoadJsonAsync()
                 .ConfigureAwait(false);
@@ -313,6 +337,47 @@ namespace OpenTween
             return media.MediaId;
         }
 
+        /// <summary>
+        /// pic.twitter.com アップロード時に JPEG への変換を回避するための加工を行う
+        /// </summary>
+        /// <remarks>
+        /// pic.twitter.com へのアップロード時に、アルファチャンネルを持たない PNG 画像が
+        /// JPEG 形式に変換され画質が低下する問題を回避します。
+        /// PNG 以外の画像や、すでにアルファチャンネルを持つ PNG 画像に対しては何もしません。
+        /// </remarks>
+        /// <returns>加工が行われた場合は true、そうでない場合は false</returns>
+        private bool AddAlphaChannelIfNeeded(Image origImage, out MemoryImage newImage)
+        {
+            newImage = null;
+
+            // PNG 画像以外に対しては何もしない
+            if (origImage.RawFormat.Guid != ImageFormat.Png.Guid)
+                return false;
+
+            using (var bitmap = new Bitmap(origImage))
+            {
+                // アルファ値が 255 以外のピクセルが含まれていた場合は何もしない
+                foreach (var x in Enumerable.Range(0, bitmap.Width))
+                {
+                    foreach (var y in Enumerable.Range(0, bitmap.Height))
+                    {
+                        if (bitmap.GetPixel(x, y).A != 255)
+                            return false;
+                    }
+                }
+
+                // 左上の 1px だけアルファ値を 254 にする
+                var pixel = bitmap.GetPixel(0, 0);
+                var newPixel = Color.FromArgb(pixel.A - 1, pixel.R, pixel.G, pixel.B);
+                bitmap.SetPixel(0, 0, newPixel);
+
+                // MemoryImage 作成時に画像はコピーされるため、この後 bitmap は破棄しても問題ない
+                newImage = MemoryImage.CopyFromImage(bitmap);
+
+                return true;
+            }
+        }
+
         public async Task SendDirectMessage(string postStr)
         {
             this.CheckAccountState();
@@ -334,16 +399,11 @@ namespace OpenTween
             this.CheckAccountState();
 
             //データ部分の生成
-            var target = id;
             var post = TabInformations.GetInstance()[id];
             if (post == null)
-            {
                 throw new WebApiException("Err:Target isn't found.");
-            }
-            if (TabInformations.GetInstance()[id].RetweetedId != null)
-            {
-                target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
-            }
+
+            var target = post.RetweetedId ?? id;  //再RTの場合は元発言をRT
 
             var response = await this.Api.StatusesRetweet(target)
                 .ConfigureAwait(false);
@@ -351,26 +411,26 @@ namespace OpenTween
             var status = await response.LoadJsonAsync()
                 .ConfigureAwait(false);
 
-            //ReTweetしたものをTLに追加
-            post = CreatePostsFromStatusData(status);
-            if (post == null)
-                throw new WebApiException("Invalid Json!");
-
             //二重取得回避
             lock (LockObj)
             {
-                if (TabInformations.GetInstance().ContainsKey(post.StatusId))
+                if (TabInformations.GetInstance().ContainsKey(status.Id))
                     return;
             }
+
             //Retweet判定
-            if (post.RetweetedId == null)
+            if (status.RetweetedStatus == null)
                 throw new WebApiException("Invalid Json!");
+
+            //ReTweetしたものをTLに追加
+            post = CreatePostsFromStatusData(status);
+            
             //ユーザー情報
             post.IsMe = true;
 
             post.IsRead = read;
             post.IsOwl = false;
-            if (_readOwnPost) post.IsRead = true;
+            if (this.ReadOwnPost) post.IsRead = true;
             post.IsDm = false;
 
             TabInformations.GetInstance().AddPost(post);
@@ -382,32 +442,9 @@ namespace OpenTween
         public long UserId
             => this.Api.CurrentUserId;
 
-        private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
-        public static MyCommon.ACCOUNT_STATE AccountState
-        {
-            get
-            {
-                return _accountState;
-            }
-            set
-            {
-                _accountState = value;
-            }
-        }
-
+        public static MyCommon.ACCOUNT_STATE AccountState { get; set; } = MyCommon.ACCOUNT_STATE.Valid;
         public bool RestrictFavCheck { get; set; }
-
-        public bool ReadOwnPost
-        {
-            get
-            {
-                return _readOwnPost;
-            }
-            set
-            {
-                _readOwnPost = value;
-            }
-        }
+        public bool ReadOwnPost { get; set; }
 
         public int FollowersCount { get; private set; }
         public int FriendsCount { get; private set; }
@@ -429,25 +466,19 @@ namespace OpenTween
         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
-        {
-            return count >= 20 && count <= GetMaxApiResultCount(type);
-        }
+            => count >= 20 && count <= GetMaxApiResultCount(type);
 
         /// <summary>
         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyMoreApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
         /// </summary>
         public static bool VerifyFirstApiResultCount(int count)
-        {
-            return count >= 20 && count <= 200;
-        }
+            => count >= 20 && count <= 200;
 
         /// <summary>
         /// WORKERTYPEに応じた取得可能な最大件数を取得する
@@ -486,42 +517,42 @@ namespace OpenTween
                 return 20;
             }
 
-            if (SettingCommon.Instance.UseAdditionalCount)
+            if (SettingManager.Common.UseAdditionalCount)
             {
                 switch (type)
                 {
                     case MyCommon.WORKERTYPE.Favorites:
-                        if (SettingCommon.Instance.FavoritesCountApi != 0)
-                            return SettingCommon.Instance.FavoritesCountApi;
+                        if (SettingManager.Common.FavoritesCountApi != 0)
+                            return SettingManager.Common.FavoritesCountApi;
                         break;
                     case MyCommon.WORKERTYPE.List:
-                        if (SettingCommon.Instance.ListCountApi != 0)
-                            return SettingCommon.Instance.ListCountApi;
+                        if (SettingManager.Common.ListCountApi != 0)
+                            return SettingManager.Common.ListCountApi;
                         break;
                     case MyCommon.WORKERTYPE.PublicSearch:
-                        if (SettingCommon.Instance.SearchCountApi != 0)
-                            return SettingCommon.Instance.SearchCountApi;
+                        if (SettingManager.Common.SearchCountApi != 0)
+                            return SettingManager.Common.SearchCountApi;
                         break;
                     case MyCommon.WORKERTYPE.UserTimeline:
-                        if (SettingCommon.Instance.UserTimelineCountApi != 0)
-                            return SettingCommon.Instance.UserTimelineCountApi;
+                        if (SettingManager.Common.UserTimelineCountApi != 0)
+                            return SettingManager.Common.UserTimelineCountApi;
                         break;
                 }
-                if (more && SettingCommon.Instance.MoreCountApi != 0)
+                if (more && SettingManager.Common.MoreCountApi != 0)
                 {
-                    return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
+                    return Math.Min(SettingManager.Common.MoreCountApi, GetMaxApiResultCount(type));
                 }
-                if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
+                if (startup && SettingManager.Common.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
                 {
-                    return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
+                    return Math.Min(SettingManager.Common.FirstCountApi, GetMaxApiResultCount(type));
                 }
             }
 
             // 上記に当てはまらない場合の共通処理
-            var count = SettingCommon.Instance.CountApi;
+            var count = SettingManager.Common.CountApi;
 
             if (type == MyCommon.WORKERTYPE.Reply)
-                count = SettingCommon.Instance.CountApiReply;
+                count = SettingManager.Common.CountApiReply;
 
             return Math.Min(count, GetMaxApiResultCount(type));
         }
@@ -615,11 +646,9 @@ namespace OpenTween
                 .ConfigureAwait(false);
 
             var item = CreatePostsFromStatusData(status);
-            if (item == null)
-                throw new WebApiException("Err:Can't create post");
 
             item.IsRead = read;
-            if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
+            if (item.IsMe && !read && this.ReadOwnPost) item.IsRead = true;
 
             return item;
         }
@@ -637,9 +666,7 @@ namespace OpenTween
         }
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status)
-        {
-            return CreatePostsFromStatusData(status, false);
-        }
+            => this.CreatePostsFromStatusData(status, false);
 
         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
         {
@@ -657,7 +684,7 @@ namespace OpenTween
                 //Id
                 post.RetweetedId = retweeted.Id;
                 //本文
-                post.TextFromApi = retweeted.Text;
+                post.TextFromApi = retweeted.FullText;
                 entities = retweeted.MergedEntities;
                 sourceHtml = retweeted.Source;
                 //Reply先
@@ -701,7 +728,7 @@ namespace OpenTween
                 {
                     post.RetweetedBy = status.User.ScreenName;
                     post.RetweetedByUserId = status.User.Id;
-                    post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.RetweetedBy.Equals(_uname, StringComparison.InvariantCultureIgnoreCase);
                 }
                 else
                 {
@@ -713,7 +740,7 @@ namespace OpenTween
             {
                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
                 //本文
-                post.TextFromApi = status.Text;
+                post.TextFromApi = status.FullText;
                 entities = status.MergedEntities;
                 sourceHtml = status.Source;
                 post.InReplyToStatusId = status.InReplyToStatusId;
@@ -743,7 +770,7 @@ namespace OpenTween
                     post.Nickname = user.Name.Trim();
                     post.ImageUrl = user.ProfileImageUrlHttps;
                     post.IsProtect = user.Protected;
-                    post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
+                    post.IsMe = post.ScreenName.Equals(_uname, StringComparison.InvariantCultureIgnoreCase);
                 }
                 else
                 {
@@ -754,13 +781,24 @@ namespace OpenTween
             }
             //HTMLに整形
             string textFromApi = post.TextFromApi;
-            post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
+
+            var quotedStatusLink = (status.RetweetedStatus ?? status).QuotedStatusPermalink;
+
+            if (quotedStatusLink != null && entities.Urls.Any(x => x.ExpandedUrl == quotedStatusLink.Expanded))
+                quotedStatusLink = null; // 移行期は entities.urls と quoted_status_permalink の両方に含まれる場合がある
+
+            post.Text = CreateHtmlAnchor(textFromApi, entities, quotedStatusLink);
             post.TextFromApi = textFromApi;
-            post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
+            post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities, quotedStatusLink);
             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+            post.AccessibleText = CreateAccessibleText(textFromApi, entities, (status.RetweetedStatus ?? status).QuotedStatus, quotedStatusLink);
+            post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+            post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
 
-            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
+            this.ExtractEntities(entities, post.ReplyToList, post.Media);
+
+            post.QuoteStatusIds = GetQuoteTweetStatusIds(entities, quotedStatusLink)
                 .Where(x => x != post.StatusId && x != post.RetweetedId)
                 .Distinct().ToArray();
 
@@ -768,12 +806,24 @@ namespace OpenTween
                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
                 .ToArray();
 
+            // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+            if (post.Text == post.TextFromApi)
+                post.Text = post.TextFromApi;
+            if (post.AccessibleText == post.TextFromApi)
+                post.AccessibleText = post.TextFromApi;
+
+            // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+            post.ScreenName = string.Intern(post.ScreenName);
+            post.Nickname = string.Intern(post.Nickname);
+            post.ImageUrl = string.Intern(post.ImageUrl);
+            post.RetweetedBy = post.RetweetedBy != null ? string.Intern(post.RetweetedBy) : null;
+
             //Source整形
-            var source = ParseSource(sourceHtml);
-            post.Source = source.Item1;
-            post.SourceUri = source.Item2;
+            var (sourceText, sourceUri) = ParseSource(sourceHtml);
+            post.Source = string.Intern(sourceText);
+            post.SourceUri = sourceUri;
 
-            post.IsReply = post.ReplyToList.Contains(_uname);
+            post.IsReply = post.RetweetedId == null && post.ReplyToList.Any(x => x.Item1 == this.UserId);
             post.IsExcludeReply = false;
 
             if (post.IsMe)
@@ -792,10 +842,13 @@ namespace OpenTween
         /// <summary>
         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
         /// </summary>
-        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
+        public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities, TwitterQuotedStatusPermalink quotedStatusLink)
         {
             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
 
+            if (quotedStatusLink != null)
+                urls = urls.Concat(new[] { quotedStatusLink.Expanded });
+
             return GetQuoteTweetStatusIds(urls);
         }
 
@@ -806,8 +859,7 @@ namespace OpenTween
                 var match = Twitter.StatusUrlRegex.Match(url);
                 if (match.Success)
                 {
-                    long statusId;
-                    if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
+                    if (long.TryParse(match.Groups["StatusId"].Value, out var statusId))
                         yield return statusId;
                 }
             }
@@ -819,34 +871,31 @@ namespace OpenTween
 
             foreach (var status in items)
             {
-                PostClass post = null;
-                post = CreatePostsFromStatusData(status);
-                if (post == null) continue;
-
-                if (minimumId == null || minimumId.Value > post.StatusId)
-                    minimumId = post.StatusId;
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
 
                 //二重取得回避
                 lock (LockObj)
                 {
                     if (tab == null)
                     {
-                        if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
+                        if (TabInformations.GetInstance().ContainsKey(status.Id)) continue;
                     }
                     else
                     {
-                        if (tab.Contains(post.StatusId)) continue;
+                        if (tab.Contains(status.Id)) continue;
                     }
                 }
 
                 //RT禁止ユーザーによるもの
                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
-                    post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
+                    status.RetweetedStatus != null && this.noRTId.Contains(status.User.Id)) continue;
+
+                var post = CreatePostsFromStatusData(status);
 
                 post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
+                if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
 
-                //非同期アイコン取得&StatusDictionaryに追加
                 if (tab != null && tab.IsInnerStorageTabType)
                     tab.AddPostQueue(post);
                 else
@@ -856,52 +905,43 @@ namespace OpenTween
             return minimumId;
         }
 
-        private long? CreatePostsFromSearchJson(TwitterSearchResult items, TabModel tab, bool read, int count, bool more)
+        private long? CreatePostsFromSearchJson(TwitterSearchResult items, PublicSearchTabModel tab, bool read, bool more)
         {
             long? minimumId = null;
 
-            foreach (var result in items.Statuses)
+            foreach (var status in items.Statuses)
             {
-                var post = CreatePostsFromStatusData(result);
-                if (post == null)
-                    continue;
-
-                if (minimumId == null || minimumId.Value > post.StatusId)
-                    minimumId = post.StatusId;
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
 
-                if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
+                if (!more && status.Id > tab.SinceId) tab.SinceId = status.Id;
                 //二重取得回避
                 lock (LockObj)
                 {
-                    if (tab == null)
-                    {
-                        if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
-                    }
-                    else
-                    {
-                        if (tab.Contains(post.StatusId)) continue;
-                    }
+                    if (tab.Contains(status.Id)) continue;
                 }
 
+                var post = CreatePostsFromStatusData(status);
+
                 post.IsRead = read;
-                if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
+                if ((post.IsMe && !read) && this.ReadOwnPost) post.IsRead = true;
 
-                //非同期アイコン取得&StatusDictionaryに追加
-                if (tab != null && tab.IsInnerStorageTabType)
-                    tab.AddPostQueue(post);
-                else
-                    TabInformations.GetInstance().AddPost(post);
+                tab.AddPostQueue(post);
             }
 
             return minimumId;
         }
 
-        private void CreateFavoritePostsFromJson(TwitterStatus[] item, bool read)
+        private long? CreateFavoritePostsFromJson(TwitterStatus[] items, bool read)
         {
             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
+            long? minimumId = null;
 
-            foreach (var status in item)
+            foreach (var status in items)
             {
+                if (minimumId == null || minimumId.Value > status.Id)
+                    minimumId = status.Id;
+
                 //二重取得回避
                 lock (LockObj)
                 {
@@ -909,12 +949,13 @@ namespace OpenTween
                 }
 
                 var post = CreatePostsFromStatusData(status, true);
-                if (post == null) continue;
 
                 post.IsRead = read;
 
                 TabInformations.GetInstance().AddPost(post);
             }
+
+            return minimumId;
         }
 
         public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, bool startup)
@@ -924,12 +965,12 @@ namespace OpenTween
             TwitterStatus[] statuses;
             if (more)
             {
-                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingCommon.Instance.IsListsIncludeRts)
+                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingManager.Common.IsListsIncludeRts)
                     .ConfigureAwait(false);
             }
             else
             {
-                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingCommon.Instance.IsListsIncludeRts)
+                statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingManager.Common.IsListsIncludeRts)
                     .ConfigureAwait(false);
             }
 
@@ -1015,8 +1056,7 @@ namespace OpenTween
                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
             foreach (var _match in ma)
             {
-                Int64 _statusId;
-                if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
+                if (Int64.TryParse(_match.Groups["StatusId"].Value, out var _statusId))
                 {
                     if (relPosts.ContainsKey(_statusId))
                         continue;
@@ -1043,7 +1083,7 @@ namespace OpenTween
 
             relPosts.Values.ToList().ForEach(p =>
             {
-                if (p.IsMe && !read && this._readOwnPost)
+                if (p.IsMe && !read && this.ReadOwnPost)
                     p.IsRead = true;
                 else
                     p.IsRead = read;
@@ -1076,7 +1116,7 @@ namespace OpenTween
             if (!TabInformations.GetInstance().ContainsTab(tab))
                 return;
 
-            var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, count, more);
+            var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, more);
 
             if (minimumId != null)
                 tab.OldestId = minimumId.Value;
@@ -1113,13 +1153,19 @@ namespace OpenTween
                     //本文
                     var textFromApi = message.Text;
                     //HTMLに整形
-                    post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
-                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
+                    post.Text = CreateHtmlAnchor(textFromApi, message.Entities, quotedStatusLink: null);
+                    post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities, quotedStatusLink: null);
                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
+                    post.AccessibleText = CreateAccessibleText(textFromApi, message.Entities, quotedStatus: null, quotedStatusLink: null);
+                    post.AccessibleText = WebUtility.HtmlDecode(post.AccessibleText);
+                    post.AccessibleText = post.AccessibleText.Replace("<3", "\u2661");
                     post.IsFav = false;
 
-                    post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
+                    this.ExtractEntities(message.Entities, post.ReplyToList, post.Media);
+
+                    post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities, quotedStatusLink: null)
+                        .Distinct().ToArray();
 
                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
@@ -1163,6 +1209,17 @@ namespace OpenTween
                     post.Nickname = user.Name.Trim();
                     post.ImageUrl = user.ProfileImageUrlHttps;
                     post.IsProtect = user.Protected;
+
+                    // メモリ使用量削減 (同一のテキストであれば同一の string インスタンスを参照させる)
+                    if (post.Text == post.TextFromApi)
+                        post.Text = post.TextFromApi;
+                    if (post.AccessibleText == post.TextFromApi)
+                        post.AccessibleText = post.TextFromApi;
+
+                    // 他の発言と重複しやすい (共通化できる) 文字列は string.Intern を通す
+                    post.ScreenName = string.Intern(post.ScreenName);
+                    post.Nickname = string.Intern(post.Nickname);
+                    post.ImageUrl = string.Intern(post.ImageUrl);
                 }
                 catch(Exception ex)
                 {
@@ -1172,7 +1229,7 @@ namespace OpenTween
                 }
 
                 post.IsRead = read;
-                if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
+                if (post.IsMe && !read && this.ReadOwnPost) post.IsRead = true;
                 post.IsReply = false;
                 post.IsExcludeReply = false;
                 post.IsDm = true;
@@ -1220,19 +1277,31 @@ namespace OpenTween
             CreateDirectMessagesFromJson(messages, gType, read);
         }
 
-        public async Task GetFavoritesApi(bool read, bool more)
+        public async Task GetFavoritesApi(bool read, FavoritesTabModel tab, bool backward)
         {
             this.CheckAccountState();
 
-            var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
+            var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, backward, false);
 
-            var statuses = await this.Api.FavoritesList(count)
-                .ConfigureAwait(false);
+            TwitterStatus[] statuses;
+            if (backward)
+            {
+                statuses = await this.Api.FavoritesList(count, maxId: tab.OldestId)
+                    .ConfigureAwait(false);
+            }
+            else
+            {
+                statuses = await this.Api.FavoritesList(count)
+                    .ConfigureAwait(false);
+            }
+
+            var minimumId = this.CreateFavoritePostsFromJson(statuses, read);
 
-            CreateFavoritePostsFromJson(statuses, read);
+            if (minimumId != null)
+                tab.OldestId = minimumId.Value;
         }
 
-        private string ReplaceTextFromApi(string text, TwitterEntities entities)
+        private string ReplaceTextFromApi(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
         {
             if (entities != null)
             {
@@ -1247,17 +1316,63 @@ namespace OpenTween
                 {
                     foreach (var m in entities.Media)
                     {
-                        if (m.AltText != null)
-                        {
-                            text = text.Replace(m.Url, string.Format(Properties.Resources.ImageAltText, m.AltText));
-                        }
-                        else
+                        if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+                    }
+                }
+            }
+
+            if (quotedStatusLink != null)
+                text += " " + quotedStatusLink.Display;
+
+            return text;
+        }
+
+        internal static string CreateAccessibleText(string text, TwitterEntities entities, TwitterStatus quotedStatus, TwitterQuotedStatusPermalink quotedStatusLink)
+        {
+            if (entities == null)
+                return text;
+
+            if (entities.Urls != null)
+            {
+                foreach (var entity in entities.Urls)
+                {
+                    if (quotedStatus != null)
+                    {
+                        var matchStatusUrl = Twitter.StatusUrlRegex.Match(entity.ExpandedUrl);
+                        if (matchStatusUrl.Success && matchStatusUrl.Groups["StatusId"].Value == quotedStatus.IdStr)
                         {
-                            if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
+                            var quotedText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+                            text = text.Replace(entity.Url, string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quotedText));
+                            continue;
                         }
                     }
+
+                    if (!string.IsNullOrEmpty(entity.DisplayUrl))
+                        text = text.Replace(entity.Url, entity.DisplayUrl);
                 }
             }
+
+            if (entities.Media != null)
+            {
+                foreach (var entity in entities.Media)
+                {
+                    if (!string.IsNullOrEmpty(entity.AltText))
+                    {
+                        text = text.Replace(entity.Url, string.Format(Properties.Resources.ImageAltText, entity.AltText));
+                    }
+                    else if (!string.IsNullOrEmpty(entity.DisplayUrl))
+                    {
+                        text = text.Replace(entity.Url, entity.DisplayUrl);
+                    }
+                }
+            }
+
+            if (quotedStatusLink != null)
+            {
+                var quoteText = CreateAccessibleText(quotedStatus.FullText, quotedStatus.MergedEntities, quotedStatus: null, quotedStatusLink: null);
+                text += " " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, quotedStatus.User.ScreenName, quoteText);
+            }
+
             return text;
         }
 
@@ -1286,15 +1401,7 @@ namespace OpenTween
             this.followerId = newFollowerIds;
             TabInformations.GetInstance().RefreshOwl(this.followerId);
 
-            this._GetFollowerResult = true;
-        }
-
-        public bool GetFollowersSuccess
-        {
-            get
-            {
-                return _GetFollowerResult;
-            }
+            this.GetFollowersSuccess = true;
         }
 
         /// <summary>
@@ -1308,15 +1415,7 @@ namespace OpenTween
             this.noRTId = await this.Api.NoRetweetIds()
                 .ConfigureAwait(false);
 
-            this._GetNoRetweetResult = true;
-        }
-
-        public bool GetNoRetweetSuccess
-        {
-            get
-            {
-                return _GetNoRetweetResult;
-            }
+            this.GetNoRetweetSuccess = true;
         }
 
         /// <summary>
@@ -1327,17 +1426,22 @@ namespace OpenTween
         {
             this.Configuration = await this.Api.Configuration()
                 .ConfigureAwait(false);
+
+            // TextConfiguration 相当の JSON を得る API が存在しないため、TransformedURLLength のみ help/configuration.json に合わせて更新する
+            this.TextConfiguration.TransformedURLLength = this.Configuration.ShortUrlLengthHttps;
         }
 
         public async Task GetListsApi()
         {
             this.CheckAccountState();
 
-            var ownedLists = await TwitterLists.GetAllItemsAsync(x => this.Api.ListsOwnerships(this.Username, cursor: x))
-                .ConfigureAwait(false);
+            var ownedLists = await TwitterLists.GetAllItemsAsync(x =>
+                this.Api.ListsOwnerships(this.Username, cursor: x, count: 1000))
+                    .ConfigureAwait(false);
 
-            var subscribedLists = await TwitterLists.GetAllItemsAsync(x => this.Api.ListsSubscriptions(this.Username, cursor: x))
-                .ConfigureAwait(false);
+            var subscribedLists = await TwitterLists.GetAllItemsAsync(x =>
+                this.Api.ListsSubscriptions(this.Username, cursor: x, count: 1000))
+                    .ConfigureAwait(false);
 
             TabInformations.GetInstance().SubscribableLists = Enumerable.Concat(ownedLists, subscribedLists)
                 .Select(x => new ListElement(x, this))
@@ -1411,7 +1515,7 @@ namespace OpenTween
             }
         }
 
-        public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
+        private void ExtractEntities(TwitterEntities entities, List<Tuple<long, string>> AtList, List<MediaInfo> media)
         {
             if (entities != null)
             {
@@ -1426,9 +1530,7 @@ namespace OpenTween
                 {
                     foreach (var ent in entities.UserMentions)
                     {
-                        var screenName = ent.ScreenName.ToLowerInvariant();
-                        if (!AtList.Contains(screenName))
-                            AtList.Add(screenName);
+                        AtList.Add(Tuple.Create(ent.Id, ent.ScreenName));
                     }
                 }
                 if (entities.Media != null)
@@ -1437,7 +1539,7 @@ namespace OpenTween
                     {
                         foreach (var ent in entities.Media)
                         {
-                            if (!media.Any(x => x.Url == ent.MediaUrl))
+                            if (!media.Any(x => x.Url == ent.MediaUrlHttps))
                             {
                                 if (ent.VideoInfo != null &&
                                     ent.Type == "animated_gif" || ent.Type == "video")
@@ -1446,22 +1548,32 @@ namespace OpenTween
                                     //    .Where(v => v.ContentType == "video/mp4")
                                     //    .OrderByDescending(v => v.Bitrate)
                                     //    .Select(v => v.Url).FirstOrDefault();
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
+                                    media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, ent.ExpandedUrl));
                                 }
                                 else
-                                    media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
+                                    media.Add(new MediaInfo(ent.MediaUrlHttps, ent.AltText, videoUrl: null));
                             }
                         }
                     }
                 }
             }
+        }
 
+        internal static string CreateHtmlAnchor(string text, TwitterEntities entities, TwitterQuotedStatusPermalink quotedStatusLink)
+        {
             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
 
             text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
             text = PreProcessUrl(text); //IDN置換
 
+            if (quotedStatusLink != null)
+            {
+                text += string.Format(" <a href=\"{0}\" title=\"{0}\">{1}</a>",
+                    WebUtility.HtmlEncode(quotedStatusLink.Url),
+                    WebUtility.HtmlEncode(quotedStatusLink.Display));
+            }
+
             return text;
         }
 
@@ -1470,10 +1582,10 @@ namespace OpenTween
         /// <summary>
         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
         /// </summary>
-        public static Tuple<string, Uri> ParseSource(string sourceHtml)
+        internal static (string SourceText, Uri SourceUri) ParseSource(string sourceHtml)
         {
             if (string.IsNullOrEmpty(sourceHtml))
-                return Tuple.Create<string, Uri>("", null);
+                return ("", null);
 
             string sourceText;
             Uri sourceUri;
@@ -1500,7 +1612,7 @@ namespace OpenTween
                 sourceUri = null;
             }
 
-            return Tuple.Create(sourceText, sourceUri);
+            return (sourceText, sourceUri);
         }
 
         public async Task<TwitterApiStatus> GetInfoApi()
@@ -1588,12 +1700,12 @@ namespace OpenTween
         {
             var matchDm = Twitter.DMSendTextRegex.Match(postText);
             if (matchDm.Success)
-                return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
+                return this.GetTextLengthRemainDM(matchDm.Groups["body"].Value);
 
-            return this.GetTextLengthRemainInternal(postText, isDm: false);
+            return this.GetTextLengthRemainWeighted(postText);
         }
 
-        private int GetTextLengthRemainInternal(string postText, bool isDm)
+        private int GetTextLengthRemainDM(string postText)
         {
             var textLength = 0;
 
@@ -1618,51 +1730,73 @@ namespace OpenTween
                 textLength += shortUrlLength - url.Length;
             }
 
-            if (isDm)
-                return this.Configuration.DmTextCharacterLimit - textLength;
-            else
-                return 140 - textLength;
+            return this.Configuration.DmTextCharacterLimit - textLength;
         }
 
-
-#region "UserStream"
-        private string trackWord_ = "";
-        public string TrackWord
+        private int GetTextLengthRemainWeighted(string postText)
         {
-            get
-            {
-                return trackWord_;
-            }
-            set
-            {
-                trackWord_ = value;
-            }
-        }
-        private bool allAtReply_ = false;
-        public bool AllAtReply
-        {
-            get
-            {
-                return allAtReply_;
-            }
-            set
+            var config = this.TextConfiguration;
+            var totalWeight = 0;
+
+            var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+
+            var pos = 0;
+            while (pos < postText.Length)
             {
-                allAtReply_ = value;
+                var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+                if (urlEntity != null)
+                {
+                    totalWeight += config.TransformedURLLength * config.Scale;
+
+                    var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
+                    pos += urlLength;
+
+                    continue;
+                }
+
+                var codepoint = postText.GetCodepointAtSafe(pos);
+                var weight = config.DefaultWeight;
+
+                foreach (var weightRange in config.Ranges)
+                {
+                    if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+                    {
+                        weight = weightRange.Weight;
+                        break;
+                    }
+                }
+
+                totalWeight += weight;
+
+                var isSurrogatePair = codepoint > 0xffff;
+                if (isSurrogatePair)
+                    pos += 2; // サロゲートペアの場合は2文字分進める
+                else
+                    pos++;
             }
+
+            var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
+
+            return remainWeight / config.Scale;
         }
 
+
+#region "UserStream"
+        public string TrackWord { get; set; } = "";
+        public bool AllAtReply { get; set; } = false;
+
         public event EventHandler NewPostFromStream;
         public event EventHandler UserStreamStarted;
         public event EventHandler UserStreamStopped;
         public event EventHandler<PostDeletedEventArgs> PostDeleted;
         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
-        private DateTime _lastUserstreamDataReceived;
+        private DateTimeUtc _lastUserstreamDataReceived;
         private TwitterUserstream userStream;
 
         public class FormattedEvent
         {
             public MyCommon.EVENTTYPE Eventtype { get; set; }
-            public DateTime CreatedAt { get; set; }
+            public DateTimeUtc CreatedAt { get; set; }
             public string Event { get; set; }
             public string Username { get; set; }
             public string Target { get; set; }
@@ -1670,18 +1804,7 @@ namespace OpenTween
             public bool IsMe { get; set; }
         }
 
-        public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
-        public List<FormattedEvent> StoredEvent
-        {
-            get
-            {
-                return storedEvent_;
-            }
-            set
-            {
-                storedEvent_ = value;
-            }
-        }
+        public List<FormattedEvent> StoredEvent { get; } = new List<FormattedEvent>();
 
         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
         {
@@ -1706,16 +1829,11 @@ namespace OpenTween
         };
 
         public bool IsUserstreamDataReceived
-        {
-            get
-            {
-                return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
-            }
-        }
+            => (DateTimeUtc.Now - this._lastUserstreamDataReceived).TotalSeconds < 31;
 
         private void userStream_StatusArrived(string line)
         {
-            this._lastUserstreamDataReceived = DateTime.Now;
+            this._lastUserstreamDataReceived = DateTimeUtc.Now;
             if (string.IsNullOrEmpty(line)) return;
 
             if (line.First() != '{' || line.Last() != '}')
@@ -1838,8 +1956,8 @@ namespace OpenTween
                 {
                     try
                     {
-                        var status = TwitterStatus.ParseJson(line);
-                        this.CreatePostsFromJson(new[] { status }, MyCommon.WORKERTYPE.UserStream, null, false);
+                        var status = TwitterStatusCompat.ParseJson(line);
+                        this.CreatePostsFromJson(new[] { status.Normalize() }, MyCommon.WORKERTYPE.UserStream, null, false);
                     }
                     catch (SerializationException ex)
                     {
@@ -1852,6 +1970,10 @@ namespace OpenTween
                 MyCommon.TraceOut(ex);
                 return;
             }
+            catch (XmlException)
+            {
+                MyCommon.TraceOut("XmlException (StatusArrived): " + line);
+            }
             catch(NullReferenceException)
             {
                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
@@ -1897,17 +2019,17 @@ namespace OpenTween
                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
             }
 
-            var evt = new FormattedEvent();
-            evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
-            evt.Event = eventData.Event;
-            evt.Username = eventData.Source.ScreenName;
-            evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
-
-            MyCommon.EVENTTYPE eventType;
-            eventTable.TryGetValue(eventData.Event, out eventType);
-            evt.Eventtype = eventType;
+            var evt = new FormattedEvent
+            {
+                CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt),
+                Event = eventData.Event,
+                Username = eventData.Source.ScreenName,
+                IsMe = eventData.Source.ScreenName.Equals(this.Username, StringComparison.InvariantCultureIgnoreCase),
+                Eventtype = eventTable.TryGetValue(eventData.Event, out var eventType) ? eventType : MyCommon.EVENTTYPE.None,
+            };
 
-            TwitterStreamEvent<TwitterStatus> tweetEvent;
+            TwitterStreamEvent<TwitterStatusCompat> tweetEvent;
+            TwitterStatus tweet;
 
             switch (eventData.Event)
             {
@@ -1917,7 +2039,7 @@ namespace OpenTween
                 case "user_suspend":
                     return;
                 case "follow":
-                    if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
+                    if (eventData.Target.ScreenName.Equals(_uname, StringComparison.InvariantCultureIgnoreCase))
                     {
                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
                     }
@@ -1935,11 +2057,12 @@ namespace OpenTween
                     return;
                 case "favorite":
                 case "unfavorite":
-                    tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
-                    evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
-                    evt.Id = tweetEvent.TargetObject.Id;
+                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+                    tweet = tweetEvent.TargetObject.Normalize();
+                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+                    evt.Id = tweet.Id;
 
-                    if (SettingCommon.Instance.IsRemoveSameEvent)
+                    if (SettingManager.Common.IsRemoveSameEvent)
                     {
                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
                             return;
@@ -1947,16 +2070,14 @@ namespace OpenTween
 
                     var tabinfo = TabInformations.GetInstance();
 
-                    PostClass post;
-                    var statusId = tweetEvent.TargetObject.Id;
-                    if (!tabinfo.Posts.TryGetValue(statusId, out post))
+                    var statusId = tweet.Id;
+                    if (!tabinfo.Posts.TryGetValue(statusId, out var post))
                         break;
 
                     if (eventData.Event == "favorite")
                     {
                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
-                        if (!favTab.Contains(post.StatusId))
-                            favTab.AddPostImmediately(post.StatusId, post.IsRead);
+                        favTab.AddPostQueue(post);
 
                         if (tweetEvent.Source.Id == this.UserId)
                         {
@@ -1966,7 +2087,7 @@ namespace OpenTween
                         {
                             post.FavoritedCount++;
 
-                            if (SettingCommon.Instance.FavEventUnread)
+                            if (SettingManager.Common.FavEventUnread)
                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
                         }
                     }
@@ -1985,11 +2106,12 @@ namespace OpenTween
                 case "quoted_tweet":
                     if (evt.IsMe) return;
 
-                    tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
-                    evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
-                    evt.Id = tweetEvent.TargetObject.Id;
+                    tweetEvent = TwitterStreamEvent<TwitterStatusCompat>.ParseJson(content);
+                    tweet = tweetEvent.TargetObject.Normalize();
+                    evt.Target = "@" + tweet.User.ScreenName + ":" + WebUtility.HtmlDecode(tweet.FullText);
+                    evt.Id = tweet.Id;
 
-                    if (SettingCommon.Instance.IsRemoveSameEvent)
+                    if (SettingManager.Common.IsRemoveSameEvent)
                     {
                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
                             return;
@@ -2043,14 +2165,10 @@ namespace OpenTween
         }
 
         private void userStream_Started()
-        {
-            this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
-        }
+            => this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
 
         private void userStream_Stopped()
-        {
-            this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
-        }
+            => this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
 
         public bool UserStreamActive
             => this.userStream != null && this.userStream.IsStreamActive;
@@ -2077,7 +2195,10 @@ namespace OpenTween
 
         public void ReconnectUserStream()
         {
-            this.StartUserStream();
+            if (this.userStream != null)
+            {
+                this.StartUserStream();
+            }
         }
 
         private class TwitterUserstream : IDisposable
@@ -2097,9 +2218,7 @@ namespace OpenTween
             private CancellationTokenSource streamCts;
 
             public TwitterUserstream(TwitterApi twitterApi)
-            {
-                this.twitterApi = twitterApi;
-            }
+                => this.twitterApi = twitterApi;
 
             public void Start(bool allAtReplies, string trackwords)
             {
@@ -2251,9 +2370,7 @@ namespace OpenTween
         public long StatusId { get; }
 
         public PostDeletedEventArgs(long statusId)
-        {
-            this.StatusId = statusId;
-        }
+            => this.StatusId = statusId;
     }
 
     public class UserStreamEventReceivedEventArgs : EventArgs
@@ -2261,8 +2378,6 @@ namespace OpenTween
         public Twitter.FormattedEvent EventData { get; }
 
         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
-        {
-            this.EventData = eventData;
-        }
+            => this.EventData = eventData;
     }
 }