From 49d58a9a6067681db700d763db85bb5697883680 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 19 Jul 2020 16:31:49 +0800
Subject: [PATCH 01/18] =?UTF-8?q?=E5=88=A0=E9=99=A4BiliSC=E9=93=BE?=
 =?UTF-8?q?=E6=8E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 frontend/src/lang/en.js         | 1 -
 frontend/src/lang/ja.js         | 1 -
 frontend/src/lang/zh.js         | 1 -
 frontend/src/layout/Sidebar.vue | 5 -----
 4 files changed, 8 deletions(-)

diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js
index 245dd609..5968f9da 100644
--- a/frontend/src/lang/en.js
+++ b/frontend/src/lang/en.js
@@ -5,7 +5,6 @@ export default {
     help: 'Help',
     projectAddress: 'Project address',
     giftRecordOfficial: 'Official Super Chat record',
-    giftRecord: 'Super Chat record'
   },
   home: {
     roomIdEmpty: "Room ID can't be empty",
diff --git a/frontend/src/lang/ja.js b/frontend/src/lang/ja.js
index 7d017a0f..8ce58f57 100644
--- a/frontend/src/lang/ja.js
+++ b/frontend/src/lang/ja.js
@@ -5,7 +5,6 @@ export default {
     help: 'ヘルプ',
     projectAddress: 'プロジェクトアドレス',
     giftRecordOfficial: '公式スーパーチャット記録',
-    giftRecord: 'スーパーチャット記録'
   },
   home: {
     roomIdEmpty: 'ルームのIDを空白にすることはできません',
diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js
index 6bc6b4ca..f9b86bad 100644
--- a/frontend/src/lang/zh.js
+++ b/frontend/src/lang/zh.js
@@ -5,7 +5,6 @@ export default {
     help: '帮助',
     projectAddress: '项目地址',
     giftRecordOfficial: '官方打赏记录',
-    giftRecord: '打赏记录'
   },
   home: {
     roomIdEmpty: '房间ID不能为空',
diff --git a/frontend/src/layout/Sidebar.vue b/frontend/src/layout/Sidebar.vue
index 2c4a45df..d87021c2 100644
--- a/frontend/src/layout/Sidebar.vue
+++ b/frontend/src/layout/Sidebar.vue
@@ -26,11 +26,6 @@
           <i class="el-icon-share"></i>{{$t('sidebar.giftRecordOfficial')}}
         </el-menu-item>
       </a>
-      <a href="https://bilisc.com/" target="_blank">
-        <el-menu-item>
-          <i class="el-icon-share"></i>{{$t('sidebar.giftRecord')}}
-        </el-menu-item>
-      </a>
       <el-submenu index="null">
         <template slot="title">
           <i class="el-icon-chat-line-square"></i>Language

From a89b64b45416f08b45d60d947b3d45257a1910df Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 19 Jul 2020 21:33:26 +0800
Subject: [PATCH 02/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/chat.py                                   |   8 +-
 .../ChatRenderer/LegacyPaidMessage.vue        | 306 -------------
 .../ChatRenderer/MembershipItem.vue           | 421 ++++++++++++++++++
 .../components/ChatRenderer/PaidMessage.vue   |   2 +-
 .../components/ChatRenderer/TextMessage.vue   |   2 +-
 .../src/components/ChatRenderer/Ticker.vue    |  12 +-
 .../src/components/ChatRenderer/index.vue     | 294 +++++++++++-
 frontend/src/lang/en.js                       |   1 +
 frontend/src/lang/ja.js                       |   1 +
 frontend/src/lang/zh.js                       |   1 +
 frontend/src/utils.js                         |   6 +-
 frontend/src/views/Room.vue                   |   4 +-
 frontend/src/views/StyleGenerator/index.vue   |  18 +-
 frontend/src/views/StyleGenerator/stylegen.js |  35 +-
 14 files changed, 757 insertions(+), 354 deletions(-)
 delete mode 100644 frontend/src/components/ChatRenderer/LegacyPaidMessage.vue
 create mode 100644 frontend/src/components/ChatRenderer/MembershipItem.vue

diff --git a/api/chat.py b/api/chat.py
index a87ceb7d..6dd152af 100644
--- a/api/chat.py
+++ b/api/chat.py
@@ -71,7 +71,7 @@ def __parse_gift(self, command):
     def __parse_buy_guard(self, command):
         data = command['data']
         return self._on_buy_guard(blivedm.GuardBuyMessage(
-            data['uid'], data['username'], None, None, None,
+            data['uid'], data['username'], data['guard_level'], None, None,
             None, None, data['start_time'], None
         ))
 
@@ -206,7 +206,8 @@ async def __on_buy_guard(self, message: blivedm.GuardBuyMessage):
             'id': id_,
             'avatarUrl': await models.avatar.get_avatar_url(message.uid),
             'timestamp': message.start_time,
-            'authorName': message.username
+            'authorName': message.username,
+            'privilegeType': message.guard_level
         })
 
     async def _on_super_chat(self, message: blivedm.SuperChatMessage):
@@ -427,7 +428,8 @@ async def send_test_message(self):
         ]
         member_data = {
             **base_data,
-            'id': uuid.uuid4().hex
+            'id': uuid.uuid4().hex,
+            'privilegeType': 3
         }
         gift_data = {
             **base_data,
diff --git a/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue b/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue
deleted file mode 100644
index ebd7cc59..00000000
--- a/frontend/src/components/ChatRenderer/LegacyPaidMessage.vue
+++ /dev/null
@@ -1,306 +0,0 @@
-<template>
-  <yt-live-chat-legacy-paid-message-renderer class="style-scope yt-live-chat-item-list-renderer">
-    <div id="card" class="style-scope yt-live-chat-legacy-paid-message-renderer">
-      <img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-legacy-paid-message-renderer"
-        :imgUrl="avatarUrl"
-      ></img-shadow>
-      <div id="content" class="style-scope yt-live-chat-legacy-paid-message-renderer">
-        <div id="content-primary-column" class="style-scope yt-live-chat-legacy-paid-message-renderer">
-          <div id="author-name" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{authorName}}</div>
-          <div id="event-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{title}}</div>
-          <div id="detail-text" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{content}}</div>
-        </div>
-        <div id="timestamp" class="style-scope yt-live-chat-legacy-paid-message-renderer">{{timeText}}</div>
-      </div>
-    </div>
-    <div id="inline-action-button-container" class="style-scope yt-live-chat-legacy-paid-message-renderer" aria-hidden="true">
-      <div id="inline-action-buttons" class="style-scope yt-live-chat-legacy-paid-message-renderer"></div>
-    </div>
-  </yt-live-chat-legacy-paid-message-renderer>
-</template>
-
-<script>
-import ImgShadow from './ImgShadow.vue'
-import * as utils from '@/utils'
-
-export default {
-  name: 'LegacyPaidMessage',
-  components: {
-    ImgShadow
-  },
-  props: {
-    avatarUrl: String,
-    authorName: String,
-    title: String,
-    content: String,
-    time: Date
-  },
-  computed: {
-    timeText() {
-      return utils.getTimeTextMinSec(this.time)
-    }
-  }
-}
-</script>
-
-<!-- yt-live-chat-legacy-paid-message-renderer -->
-<style>
-canvas.yt-live-chat-legacy-paid-message-renderer, caption.yt-live-chat-legacy-paid-message-renderer, center.yt-live-chat-legacy-paid-message-renderer, cite.yt-live-chat-legacy-paid-message-renderer, code.yt-live-chat-legacy-paid-message-renderer, dd.yt-live-chat-legacy-paid-message-renderer, del.yt-live-chat-legacy-paid-message-renderer, dfn.yt-live-chat-legacy-paid-message-renderer, div.yt-live-chat-legacy-paid-message-renderer, dl.yt-live-chat-legacy-paid-message-renderer, dt.yt-live-chat-legacy-paid-message-renderer, em.yt-live-chat-legacy-paid-message-renderer, embed.yt-live-chat-legacy-paid-message-renderer, fieldset.yt-live-chat-legacy-paid-message-renderer, font.yt-live-chat-legacy-paid-message-renderer, form.yt-live-chat-legacy-paid-message-renderer, h1.yt-live-chat-legacy-paid-message-renderer, h2.yt-live-chat-legacy-paid-message-renderer, h3.yt-live-chat-legacy-paid-message-renderer, h4.yt-live-chat-legacy-paid-message-renderer, h5.yt-live-chat-legacy-paid-message-renderer, h6.yt-live-chat-legacy-paid-message-renderer, hr.yt-live-chat-legacy-paid-message-renderer, i.yt-live-chat-legacy-paid-message-renderer, iframe.yt-live-chat-legacy-paid-message-renderer, img.yt-live-chat-legacy-paid-message-renderer, ins.yt-live-chat-legacy-paid-message-renderer, kbd.yt-live-chat-legacy-paid-message-renderer, label.yt-live-chat-legacy-paid-message-renderer, legend.yt-live-chat-legacy-paid-message-renderer, li.yt-live-chat-legacy-paid-message-renderer, menu.yt-live-chat-legacy-paid-message-renderer, object.yt-live-chat-legacy-paid-message-renderer, ol.yt-live-chat-legacy-paid-message-renderer, p.yt-live-chat-legacy-paid-message-renderer, pre.yt-live-chat-legacy-paid-message-renderer, q.yt-live-chat-legacy-paid-message-renderer, s.yt-live-chat-legacy-paid-message-renderer, samp.yt-live-chat-legacy-paid-message-renderer, small.yt-live-chat-legacy-paid-message-renderer, span.yt-live-chat-legacy-paid-message-renderer, strike.yt-live-chat-legacy-paid-message-renderer, strong.yt-live-chat-legacy-paid-message-renderer, sub.yt-live-chat-legacy-paid-message-renderer, sup.yt-live-chat-legacy-paid-message-renderer, table.yt-live-chat-legacy-paid-message-renderer, tbody.yt-live-chat-legacy-paid-message-renderer, td.yt-live-chat-legacy-paid-message-renderer, tfoot.yt-live-chat-legacy-paid-message-renderer, th.yt-live-chat-legacy-paid-message-renderer, thead.yt-live-chat-legacy-paid-message-renderer, tr.yt-live-chat-legacy-paid-message-renderer, tt.yt-live-chat-legacy-paid-message-renderer, u.yt-live-chat-legacy-paid-message-renderer, ul.yt-live-chat-legacy-paid-message-renderer, var.yt-live-chat-legacy-paid-message-renderer {
-  margin: 0;
-  padding: 0;
-  border: 0;
-  background: transparent;
-}
-
-.yt-live-chat-legacy-paid-message-renderer[hidden] {
-  display: none !important;
-}
-
-#timestamp.yt-live-chat-legacy-paid-message-renderer {
-  display: var(--yt-live-chat-item-timestamp-display, inline);
-  margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
-  color: var(--yt-live-chat-tertiary-text-color);
-  font-size: 11px;
-}
-
-#author-photo.yt-live-chat-legacy-paid-message-renderer {
-  display: block;
-  margin-right: 16px;
-  overflow: hidden;
-  border-radius: 50%;
-  -ms-flex: none;
-  -webkit-flex: none;
-  flex: none;
-}
-
-#menu-button.yt-live-chat-legacy-paid-message-renderer {
-  width: 40px;
-  height: 40px;
-  padding: 8px;
-}
-
-#menu.yt-live-chat-legacy-paid-message-renderer {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  right: 0;
-  transform: translateX(100px);
-}
-
-yt-live-chat-legacy-paid-message-renderer:hover #menu.yt-live-chat-legacy-paid-message-renderer, yt-live-chat-legacy-paid-message-renderer[menu-visible] #menu.yt-live-chat-legacy-paid-message-renderer {
-  transform: none;
-}
-
-yt-live-chat-legacy-paid-message-renderer:focus-within #menu.yt-live-chat-legacy-paid-message-renderer {
-  transform: none;
-}
-
-#inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
-  position: absolute;
-  top: -4px;
-  right: 0;
-  bottom: -4px;
-  left: 0;
-  background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
-  display: none;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-  -ms-flex-pack: center;
-  -webkit-justify-content: center;
-  justify-content: center;
-}
-
-yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
-  display: flex;
-  -ms-flex-direction: row;
-  -webkit-flex-direction: row;
-  flex-direction: row;
-  display: var(--yt-live-chat-inline-action-button-container-display, none);
-}
-
-yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-legacy-paid-message-renderer {
-  display: none;
-}
-
-yt-live-chat-legacy-paid-message-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-legacy-paid-message-renderer {
-  display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
-}
-
-#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer, #additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
-  --yt-button-icon-size: 36px;
-  --yt-button-icon-padding: 6px;
-  color: hsl(0, 0%, 100%);
-  border-radius: 2px;
-}
-
-#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
-  background: hsla(0, 0%, 6.7%, .8);
-}
-
-#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>.yt-live-chat-legacy-paid-message-renderer:hover {
-  background: hsl(0, 0%, 6.7%);
-}
-
-#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer {
-  color: var(--yt-live-chat-additional-inline-action-button-color);
-  background: var(--yt-live-chat-additional-inline-action-button-background-color);
-}
-
-#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>.yt-live-chat-legacy-paid-message-renderer:hover {
-  background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
-}
-
-#additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer:not(:empty) {
-  margin-left: 32px;
-}
-
-#inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer:not(:first-child), #additional-inline-action-buttons.yt-live-chat-legacy-paid-message-renderer>*.yt-live-chat-legacy-paid-message-renderer:not(:first-child) {
-  margin-left: 8px;
-}
-
-yt-live-chat-legacy-paid-message-renderer {
-  position: relative;
-  display: block;
-  --yt-live-chat-sponsor-color: #0f9d58;
-  --yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
-  padding: 4px 24px;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] {
-  padding: 0;
-}
-
-#card.yt-live-chat-legacy-paid-message-renderer {
-  position: relative;
-  padding: 8px 16px;
-  background-color: var(--yt-live-chat-sponsor-color);
-  border-radius: 4px;
-  color: #fff;
-  font-size: 14px;
-  min-height: 40px;
-  display: flex;
-  -ms-flex-direction: row;
-  -webkit-flex-direction: row;
-  flex-direction: row;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #card.yt-live-chat-legacy-paid-message-renderer {
-  border-radius: 0;
-  box-shadow: none;
-  background-color: var(--yt-live-chat-background-color);
-  color: rgba(0, 0, 0, 0.87);
-}
-
-#author-photo.yt-live-chat-legacy-paid-message-renderer {
-  -ms-align-self: flex-start;
-  -webkit-align-self: flex-start;
-  align-self: flex-start;
-}
-
-#author-name.yt-live-chat-legacy-paid-message-renderer {
-  display: none;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #author-name.yt-live-chat-legacy-paid-message-renderer {
-  display: block;
-  margin-right: 8px;
-  color: var(--yt-live-chat-secondary-text-color);
-  font-weight: 500;
-}
-
-#content.yt-live-chat-legacy-paid-message-renderer {
-  -ms-flex: 1 1 0.000000001px;
-  -webkit-flex: 1;
-  flex: 1;
-  -webkit-flex-basis: 0.000000001px;
-  flex-basis: 0.000000001px;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #content.yt-live-chat-legacy-paid-message-renderer {
-  display: flex;
-  -ms-flex-direction: column;
-  -webkit-flex-direction: column;
-  flex-direction: column;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #content-primary-column.yt-live-chat-legacy-paid-message-renderer {
-  display: flex;
-  -ms-flex-direction: row;
-  -webkit-flex-direction: row;
-  flex-direction: row;
-  -ms-flex-align: baseline;
-  -webkit-align-items: baseline;
-  align-items: baseline;
-}
-
-#event-text.yt-live-chat-legacy-paid-message-renderer {
-  color: rgba(255, 255, 255, 0.7);
-  font-weight: 500;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #event-text.yt-live-chat-legacy-paid-message-renderer {
-  display: inline;
-  height: 24px;
-  min-width: 16px;
-  border-radius: 12px;
-  margin-right: 8px;
-  padding: 0 12px;
-  background-color: var(--yt-live-chat-sponsor-color);
-  color: hsl(0, 0%, 100%);
-  display: inline-flex;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-  -ms-flex-pack: center;
-  -webkit-justify-content: center;
-  justify-content: center;
-  font-size: 1.2rem;
-  font-weight: 500;
-  line-height: 1.2rem;
-}
-
-#detail-text.yt-live-chat-legacy-paid-message-renderer {
-  font-size: 15px;
-  word-wrap: break-word;
-  word-break: break-word;
-}
-
-#detail-text.yt-live-chat-legacy-paid-message-renderer .emoji.yt-live-chat-legacy-paid-message-renderer {
-  width: var(--yt-live-chat-emoji-size);
-  height: var(--yt-live-chat-emoji-size);
-  margin: -1px 2px 1px 2px;
-  vertical-align: middle;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #detail-text.yt-live-chat-legacy-paid-message-renderer {
-  display: none;
-}
-
-a.yt-live-chat-legacy-paid-message-renderer {
-  display: inline;
-  text-decoration: underline;
-}
-
-#detail-text.yt-live-chat-legacy-paid-message-renderer a.yt-live-chat-legacy-paid-message-renderer {
-  word-break: break-all;
-}
-
-#detail-text.yt-live-chat-legacy-paid-message-renderer a.yt-live-chat-legacy-paid-message-renderer .mention.yt-live-chat-legacy-paid-message-renderer {
-  text-decoration: underline;
-}
-
-#menu.yt-live-chat-legacy-paid-message-renderer {
-  background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-color) 100%);
-  border-radius: 0 4px 4px 0;
-}
-
-yt-live-chat-legacy-paid-message-renderer[dashboard-money-feed] #menu.yt-live-chat-legacy-paid-message-renderer {
-  margin-top: 8px;
-  background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
-}
-</style>
diff --git a/frontend/src/components/ChatRenderer/MembershipItem.vue b/frontend/src/components/ChatRenderer/MembershipItem.vue
new file mode 100644
index 00000000..3bd5525e
--- /dev/null
+++ b/frontend/src/components/ChatRenderer/MembershipItem.vue
@@ -0,0 +1,421 @@
+<template>
+  <yt-live-chat-membership-item-renderer class="style-scope yt-live-chat-item-list-renderer" show-only-header>
+    <div id="card" class="style-scope yt-live-chat-membership-item-renderer">
+      <div id="header" class="style-scope yt-live-chat-membership-item-renderer">
+        <img-shadow id="author-photo" height="40" width="40" class="style-scope yt-live-chat-membership-item-renderer"
+          :imgUrl="avatarUrl"
+        ></img-shadow>
+        <div id="header-content" class="style-scope yt-live-chat-membership-item-renderer">
+          <div id="header-content-primary-column" class="style-scope yt-live-chat-membership-item-renderer">
+            <div id="header-content-inner-column" class="style-scope yt-live-chat-membership-item-renderer">
+              <yt-live-chat-author-chip class="style-scope yt-live-chat-membership-item-renderer">
+                <span id="author-name" dir="auto" class="member style-scope yt-live-chat-author-chip">{{
+                  authorName
+                  }}<!-- 这里是已验证勋章 -->
+                  <span id="chip-badges" class="style-scope yt-live-chat-author-chip"></span>
+                </span>
+                <span id="chat-badges" class="style-scope yt-live-chat-author-chip">
+                  <author-badge class="style-scope yt-live-chat-author-chip"
+                    :isAdmin="false" :privilegeType="privilegeType"
+                  ></author-badge>
+                </span>
+              </yt-live-chat-author-chip>
+            </div>
+            <div id="header-subtext" class="style-scope yt-live-chat-membership-item-renderer">{{title}}</div>
+          </div>
+          <div id="timestamp" class="style-scope yt-live-chat-membership-item-renderer">{{timeText}}</div>
+        </div>
+      </div>
+    </div>
+  </yt-live-chat-membership-item-renderer>
+</template>
+
+<script>
+import ImgShadow from './ImgShadow.vue'
+import AuthorBadge from './AuthorBadge.vue'
+import * as utils from '@/utils'
+
+export default {
+  name: 'MembershipItem',
+  components: {
+    ImgShadow,
+    AuthorBadge
+  },
+  props: {
+    avatarUrl: String,
+    authorName: String,
+    privilegeType: Number,
+    title: String,
+    time: Date
+  },
+  computed: {
+    timeText() {
+      return utils.getTimeTextHourMin(this.time)
+    }
+  }
+}
+</script>
+
+<!-- yt-live-chat-membership-item-renderer -->
+<style>
+#timestamp.yt-live-chat-membership-item-renderer {
+  display: var(--yt-live-chat-item-timestamp-display, inline);
+  margin: var(--yt-live-chat-item-timestamp-margin, 0 8px 0 0);
+  color: var(--yt-live-chat-tertiary-text-color);
+  font-size: 11px;
+}
+
+#author-photo.yt-live-chat-membership-item-renderer {
+  display: block;
+  margin-right: 16px;
+
+  overflow: hidden;
+  border-radius: 50%;
+
+  -ms-flex: var(--layout-flex-none_-_-ms-flex);
+  -webkit-flex: var(--layout-flex-none_-_-webkit-flex);
+  flex: var(--layout-flex-none_-_flex);
+}
+
+#menu-button.yt-live-chat-membership-item-renderer {
+  width: var(--yt-live-chat-32px-icon-button_-_width);
+  height: var(--yt-live-chat-32px-icon-button_-_height);
+  padding: var(--yt-live-chat-32px-icon-button_-_padding);
+}
+
+#menu.yt-live-chat-membership-item-renderer {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 0;
+
+
+  transform: translateX(100px);
+}
+
+yt-live-chat-membership-item-renderer:hover #menu.yt-live-chat-membership-item-renderer,
+yt-live-chat-membership-item-renderer[menu-visible] #menu.yt-live-chat-membership-item-renderer {
+  transform: none;
+}
+
+yt-live-chat-membership-item-renderer:focus-within #menu.yt-live-chat-membership-item-renderer {
+  transform: none;
+}
+
+#inline-action-button-container.yt-live-chat-membership-item-renderer {
+  position: absolute;
+  top: -4px;
+  right: 0;
+  bottom: -4px;
+  left: 0;
+
+  background-color: var(--yt-live-chat-moderation-mode-hover-background-color);
+  display: none;
+
+  -ms-flex-align: var(--layout-center-center_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-center-center_-_-webkit-align-items);
+  align-items: var(--layout-center-center_-_align-items);
+  -ms-flex-pack: var(--layout-center-center_-_-ms-flex-pack);
+  -webkit-justify-content: var(--layout-center-center_-_-webkit-justify-content);
+  justify-content: var(--layout-center-center_-_justify-content);
+}
+
+yt-live-chat-membership-item-renderer[has-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-membership-item-renderer {
+  display: var(--layout-horizontal_-_display);
+  -ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
+  flex-direction: var(--layout-horizontal_-_flex-direction);
+
+
+  display: var(--yt-live-chat-inline-action-button-container-display, none);
+}
+
+yt-live-chat-membership-item-renderer[has-inline-action-buttons][hide-inline-action-buttons]:hover #inline-action-button-container.yt-live-chat-membership-item-renderer {
+  display: none;
+}
+
+yt-live-chat-membership-item-renderer[has-inline-action-buttons]:hover #menu.yt-live-chat-membership-item-renderer {
+  display: var(--yt-live-chat-item-with-inline-actions-context-menu-display, block);
+}
+
+#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer,
+#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
+  --yt-button-icon-size: 36px;
+  --yt-button-icon-padding: 6px;
+
+  color: var(--yt-white);
+  border-radius: 2px;
+}
+
+#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
+  background: var(--yt-luna-black-opacity-lighten-1);
+}
+
+#inline-action-buttons.yt-live-chat-membership-item-renderer>.yt-live-chat-membership-item-renderer:hover {
+  background: var(--yt-luna-black);
+}
+
+#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer {
+  color: var(--yt-live-chat-additional-inline-action-button-color);
+  background: var(--yt-live-chat-additional-inline-action-button-background-color);
+}
+
+#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>.yt-live-chat-membership-item-renderer:hover {
+  background: var(--yt-live-chat-additional-inline-action-button-background-color-hover);
+}
+
+#additional-inline-action-buttons.yt-live-chat-membership-item-renderer:not(:empty) {
+  margin-left: 32px;
+}
+
+#inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer:not(:first-child),
+#additional-inline-action-buttons.yt-live-chat-membership-item-renderer>*.yt-live-chat-membership-item-renderer:not(:first-child) {
+  margin-left: 8px;
+}
+
+yt-live-chat-membership-item-renderer {
+  position: relative;
+  display: block;
+
+  --yt-live-chat-sponsor-header-color: #0a8043;
+  --yt-live-chat-sponsor-color: #0f9d58;
+  --yt-live-chat-sponsor-text-color: #fff;
+  --yt-live-chat-item-timestamp-display: var(--yt-live-chat-paid-message-timestamp-display, none);
+
+  padding: 4px 24px;
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] {
+  padding: 0;
+
+  --yt-live-chat-item-timestamp-display: block;
+}
+
+#card.yt-live-chat-membership-item-renderer {
+  overflow: hidden;
+  font-size: 14px;
+  border-radius: 4px;
+
+  display: var(--layout-vertical_-_display);
+  -ms-flex-direction: var(--layout-vertical_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-vertical_-_-webkit-flex-direction);
+  flex-direction: var(--layout-vertical_-_flex-direction);
+  box-shadow: var(--shadow-elevation-2dp_-_box-shadow);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #card.yt-live-chat-membership-item-renderer {
+  border-radius: 0;
+  box-shadow: none;
+}
+
+#header.yt-live-chat-membership-item-renderer {
+  position: relative;
+
+  background-color: var(--yt-live-chat-sponsor-header-color);
+  padding: 8px 16px;
+  color: #fff;
+  min-height: 20px;
+
+  display: var(--layout-horizontal_-_display);
+  -ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
+  flex-direction: var(--layout-horizontal_-_flex-direction);
+  -ms-flex-align: var(--layout-center_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-center_-_-webkit-align-items);
+  align-items: var(--layout-center_-_align-items);
+}
+
+yt-live-chat-membership-item-renderer[show-only-header] #header.yt-live-chat-membership-item-renderer {
+  background-color: var(--yt-live-chat-sponsor-color);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #header.yt-live-chat-membership-item-renderer {
+  color: var(--yt-live-chat-secondary-text-color);
+  background-color: var(--yt-live-chat-background-color);
+  -ms-flex-align: var(--layout-start_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-start_-_-webkit-align-items);
+  align-items: var(--layout-start_-_align-items);
+}
+
+#header-content.yt-live-chat-membership-item-renderer {
+  display: var(--layout-horizontal_-_display);
+  -ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
+  flex-direction: var(--layout-horizontal_-_flex-direction);
+  -ms-flex-pack: var(--layout-justified_-_-ms-flex-pack);
+  -webkit-justify-content: var(--layout-justified_-_-webkit-justify-content);
+  justify-content: var(--layout-justified_-_justify-content);
+  -ms-flex: var(--layout-flex_-_-ms-flex);
+  -webkit-flex: var(--layout-flex_-_-webkit-flex);
+  flex: var(--layout-flex_-_flex);
+  -webkit-flex-basis: var(--layout-flex_-_-webkit-flex-basis);
+  flex-basis: var(--layout-flex_-_flex-basis);
+  -ms-flex-align: var(--layout-baseline_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-baseline_-_-webkit-align-items);
+  align-items: var(--layout-baseline_-_align-items);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-content.yt-live-chat-membership-item-renderer {
+  display: var(--layout-vertical_-_display);
+  -ms-flex-direction: var(--layout-vertical_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-vertical_-_-webkit-flex-direction);
+  flex-direction: var(--layout-vertical_-_flex-direction);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-content-inner-column.yt-live-chat-membership-item-renderer {
+  margin-bottom: 4px;
+
+  display: var(--layout-horizontal_-_display);
+  -ms-flex-direction: var(--layout-horizontal_-_-ms-flex-direction);
+  -webkit-flex-direction: var(--layout-horizontal_-_-webkit-flex-direction);
+  flex-direction: var(--layout-horizontal_-_flex-direction);
+  -ms-flex-align: var(--layout-center_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-center_-_-webkit-align-items);
+  align-items: var(--layout-center_-_align-items);
+  -ms-flex: var(--layout-flex-none_-_-ms-flex);
+  -webkit-flex: var(--layout-flex-none_-_-webkit-flex);
+  flex: var(--layout-flex-none_-_flex);
+}
+
+#author-photo.yt-live-chat-membership-item-renderer {
+  width: 40px;
+  height: 40px;
+}
+
+yt-icon#author-photo.yt-live-chat-membership-item-renderer {
+  display: none;
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] yt-icon#author-photo.yt-live-chat-membership-item-renderer {
+  display: block;
+}
+
+yt-live-chat-membership-item-renderer:not([dashboard-money-feed]) yt-live-chat-author-chip.yt-live-chat-membership-item-renderer {
+  --yt-live-chat-sponsor-color: var(--yt-live-chat-sponsor-text-color);
+  --yt-live-chat-secondary-text-color: var(--yt-live-chat-sponsor-text-color);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] yt-live-chat-author-chip.yt-live-chat-membership-item-renderer {
+  margin-right: 8px;
+  font-weight: 500;
+  --yt-live-chat-sponsor-color: var(--yt-live-chat-secondary-text-color);
+}
+
+#header-subtext.yt-live-chat-membership-item-renderer {
+  margin-top: 2px;
+  color: rgba(255, 255, 255, 0.7);
+  font-weight: 500;
+  font-size: 15px;
+}
+
+#header-subtext.yt-live-chat-membership-item-renderer:empty {
+  display: none;
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-subtext.yt-live-chat-membership-item-renderer {
+  margin: 4px 0 13px;
+  font-size: 11px;
+  font-weight: normal;
+  color: var(--yt-live-chat-secondary-text-color);
+}
+
+#header-primary-text.yt-live-chat-membership-item-renderer {
+  word-wrap: break-word;
+  word-break: break-word;
+  font-weight: 500;
+  color: rgba(255, 255, 255, 1);
+}
+
+#header-primary-text.yt-live-chat-membership-item-renderer:empty {
+  display: none;
+}
+
+yt-live-chat-membership-item-renderer[has-primary-header-text]:not([dashboard-money-feed]) yt-live-chat-author-chip.yt-live-chat-membership-item-renderer,
+yt-live-chat-membership-item-renderer[has-primary-header-text]:not([dashboard-money-feed]) #header-subtext.yt-live-chat-membership-item-renderer {
+  font-size: 12px;
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #header-primary-text.yt-live-chat-membership-item-renderer {
+  display: inline;
+  height: 24px;
+  min-width: 16px;
+  border-radius: 12px;
+  margin-right: 8px;
+  padding: 0 12px;
+  background-color: var(--yt-live-chat-sponsor-color);
+  color: var(--yt-white);
+  display: var(--layout-inline_-_display, inline);
+  -ms-flex-align: var(--layout-center-center_-_-ms-flex-align);
+  -webkit-align-items: var(--layout-center-center_-_-webkit-align-items);
+  align-items: var(--layout-center-center_-_align-items);
+  -ms-flex-pack: var(--layout-center-center_-_-ms-flex-pack);
+  -webkit-justify-content: var(--layout-center-center_-_-webkit-justify-content);
+  justify-content: var(--layout-center-center_-_justify-content);
+  font-size: var(--ytd-badge_-_font-size);
+  font-weight: var(--ytd-badge_-_font-weight);
+  line-height: var(--ytd-badge_-_line-height);
+}
+
+#content.yt-live-chat-membership-item-renderer {
+  background-color: var(--yt-live-chat-sponsor-color);
+  color: var(--yt-live-chat-sponsor-text-color);
+  padding: 8px 16px;
+  word-wrap: break-word;
+  word-break: break-word;
+  font-size: 15px;
+  line-height: 20px;
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #content.yt-live-chat-membership-item-renderer {
+  background-color: unset;
+  font-size: unset;
+  color: var(--yt-live-chat-secondary-text-color);
+  padding: 0 0 16px 72px;
+}
+
+#content.yt-live-chat-membership-item-renderer img.yt-live-chat-membership-item-renderer {
+  width: var(--yt-live-chat-emoji-size);
+  height: var(--yt-live-chat-emoji-size);
+
+  margin: -1px 2px 1px 2px;
+  vertical-align: middle;
+}
+
+yt-live-chat-membership-item-renderer[show-only-header] #content.yt-live-chat-membership-item-renderer,
+#deleted-state.yt-live-chat-membership-item-renderer:empty {
+  display: none;
+}
+
+#deleted-state.yt-live-chat-membership-item-renderer {
+  display: block;
+  font-style: italic;
+  opacity: 0.7;
+}
+
+a.yt-live-chat-membership-item-renderer {
+  display: inline;
+  text-decoration: underline;
+}
+
+#message.yt-live-chat-membership-item-renderer a.yt-live-chat-membership-item-renderer {
+  word-break: break-all;
+}
+
+#message.yt-live-chat-membership-item-renderer a.yt-live-chat-membership-item-renderer .mention.yt-live-chat-membership-item-renderer {
+  text-decoration: underline;
+}
+
+#menu.yt-live-chat-membership-item-renderer {
+  background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-header-color) 100%);
+  border-radius: 0 4px 4px 0;
+}
+
+yt-live-chat-membership-item-renderer[show-only-header] #menu.yt-live-chat-membership-item-renderer {
+  background: linear-gradient(to right, transparent, var(--yt-live-chat-sponsor-color) 100%);
+}
+
+yt-live-chat-membership-item-renderer[dashboard-money-feed] #menu.yt-live-chat-membership-item-renderer {
+  margin-top: 8px;
+  background: linear-gradient(to right, transparent, var(--yt-live-chat-background-color) 40%);
+}
+</style>
diff --git a/frontend/src/components/ChatRenderer/PaidMessage.vue b/frontend/src/components/ChatRenderer/PaidMessage.vue
index 5c87d2e2..4809d7d2 100644
--- a/frontend/src/components/ChatRenderer/PaidMessage.vue
+++ b/frontend/src/components/ChatRenderer/PaidMessage.vue
@@ -56,7 +56,7 @@ export default {
       return 'CN¥' + utils.formatCurrency(this.price)
     },
     timeText() {
-      return utils.getTimeTextMinSec(this.time)
+      return utils.getTimeTextHourMin(this.time)
     }
   }
 }
diff --git a/frontend/src/components/ChatRenderer/TextMessage.vue b/frontend/src/components/ChatRenderer/TextMessage.vue
index c3c3a101..6d0ebd7c 100644
--- a/frontend/src/components/ChatRenderer/TextMessage.vue
+++ b/frontend/src/components/ChatRenderer/TextMessage.vue
@@ -54,7 +54,7 @@ export default {
   },
   computed: {
     timeText() {
-      return utils.getTimeTextMinSec(this.time)
+      return utils.getTimeTextHourMin(this.time)
     },
     authorTypeText() {
       return constants.AUTHOR_TYPE_TO_TEXT[this.authorType]
diff --git a/frontend/src/components/ChatRenderer/Ticker.vue b/frontend/src/components/ChatRenderer/Ticker.vue
index 28abd494..0f8e4599 100644
--- a/frontend/src/components/ChatRenderer/Ticker.vue
+++ b/frontend/src/components/ChatRenderer/Ticker.vue
@@ -26,11 +26,11 @@
       </div>
     </div>
     <template v-if="pinnedMessage">
-      <legacy-paid-message :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
+      <membership-item :key="pinnedMessage.id" v-if="pinnedMessage.type === MESSAGE_TYPE_MEMBER"
         class="style-scope yt-live-chat-ticker-renderer"
-        :avatarUrl="pinnedMessage.avatarUrl" :title="pinnedMessage.title" :content="pinnedMessage.content"
-        :time="pinnedMessage.time"
-      ></legacy-paid-message>
+        :avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName" :privilegeType="pinnedMessage.privilegeType"
+        :title="pinnedMessage.title" :time="pinnedMessage.time"
+      ></membership-item>
       <paid-message :key="pinnedMessage.id" v-else
         class="style-scope yt-live-chat-ticker-renderer"
         :price="pinnedMessage.price" :avatarUrl="pinnedMessage.avatarUrl" :authorName="pinnedMessage.authorName"
@@ -44,7 +44,7 @@
 import * as config from '@/api/config'
 import {formatCurrency} from '@/utils'
 import ImgShadow from './ImgShadow.vue'
-import LegacyPaidMessage from './LegacyPaidMessage.vue'
+import MembershipItem from './MembershipItem.vue'
 import PaidMessage from './PaidMessage.vue'
 import * as constants from './constants'
 
@@ -52,7 +52,7 @@ export default {
   name: 'Ticker',
   components: {
     ImgShadow,
-    LegacyPaidMessage,
+    MembershipItem,
     PaidMessage
   },
   props: {
diff --git a/frontend/src/components/ChatRenderer/index.vue b/frontend/src/components/ChatRenderer/index.vue
index 0f9f91ea..dce06cf5 100644
--- a/frontend/src/components/ChatRenderer/index.vue
+++ b/frontend/src/components/ChatRenderer/index.vue
@@ -23,11 +23,11 @@
                 :price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
                 :time="message.time" :content="getGiftShowContent(message)"
               ></paid-message>
-              <legacy-paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
+              <membership-item :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
                 class="style-scope yt-live-chat-item-list-renderer"
-                :avatarUrl="message.avatarUrl" :title="message.title" :content="message.content"
-                :time="message.time"
-              ></legacy-paid-message>
+                :avatarUrl="message.avatarUrl" :authorName="message.authorName" :privilegeType="message.privilegeType"
+                :title="message.title" :time="message.time"
+              ></membership-item>
               <paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
                 class="style-scope yt-live-chat-item-list-renderer"
                 :price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
@@ -45,7 +45,7 @@
 import * as config from '@/api/config'
 import Ticker from './Ticker.vue'
 import TextMessage from './TextMessage.vue'
-import LegacyPaidMessage from './LegacyPaidMessage.vue'
+import MembershipItem from './MembershipItem.vue'
 import PaidMessage from './PaidMessage.vue'
 import * as constants from './constants'
 
@@ -57,7 +57,7 @@ export default {
   components: {
     Ticker,
     TextMessage,
-    LegacyPaidMessage,
+    MembershipItem,
     PaidMessage
   },
   props: {
@@ -628,6 +628,288 @@ html:not(.style-scope) {
   --yt-pdg-paid-stickers-author-name-font-size: 13px;
   --yt-pdg-paid-stickers-margin-left: 38px;
 }
+
+html:not(.style-scope) {
+  --layout_-_display: flex;
+  ;
+
+  --layout-inline_-_display: inline-flex;
+  ;
+
+  --layout-horizontal_-_display: var(--layout_-_display);
+  --layout-horizontal_-_-ms-flex-direction: row;
+  --layout-horizontal_-_-webkit-flex-direction: row;
+  --layout-horizontal_-_flex-direction: row;
+  ;
+
+  --layout-horizontal-reverse_-_display: var(--layout_-_display);
+  --layout-horizontal-reverse_-_-ms-flex-direction: row-reverse;
+  --layout-horizontal-reverse_-_-webkit-flex-direction: row-reverse;
+  --layout-horizontal-reverse_-_flex-direction: row-reverse;
+  ;
+
+  --layout-vertical_-_display: var(--layout_-_display);
+  --layout-vertical_-_-ms-flex-direction: column;
+  --layout-vertical_-_-webkit-flex-direction: column;
+  --layout-vertical_-_flex-direction: column;
+  ;
+
+  --layout-vertical-reverse_-_display: var(--layout_-_display);
+  --layout-vertical-reverse_-_-ms-flex-direction: column-reverse;
+  --layout-vertical-reverse_-_-webkit-flex-direction: column-reverse;
+  --layout-vertical-reverse_-_flex-direction: column-reverse;
+  ;
+
+  --layout-wrap_-_-ms-flex-wrap: wrap;
+  --layout-wrap_-_-webkit-flex-wrap: wrap;
+  --layout-wrap_-_flex-wrap: wrap;
+  ;
+
+  --layout-wrap-reverse_-_-ms-flex-wrap: wrap-reverse;
+  --layout-wrap-reverse_-_-webkit-flex-wrap: wrap-reverse;
+  --layout-wrap-reverse_-_flex-wrap: wrap-reverse;
+  ;
+
+  --layout-flex-auto_-_-ms-flex: 1 1 auto;
+  --layout-flex-auto_-_-webkit-flex: 1 1 auto;
+  --layout-flex-auto_-_flex: 1 1 auto;
+  ;
+
+  --layout-flex-none_-_-ms-flex: none;
+  --layout-flex-none_-_-webkit-flex: none;
+  --layout-flex-none_-_flex: none;
+  ;
+
+  --layout-flex_-_-ms-flex: 1 1 0.000000001px;
+  --layout-flex_-_-webkit-flex: 1;
+  --layout-flex_-_flex: 1;
+  --layout-flex_-_-webkit-flex-basis: 0.000000001px;
+  --layout-flex_-_flex-basis: 0.000000001px;
+  ;
+
+  --layout-flex-2_-_-ms-flex: 2;
+  --layout-flex-2_-_-webkit-flex: 2;
+  --layout-flex-2_-_flex: 2;
+  ;
+
+  --layout-flex-3_-_-ms-flex: 3;
+  --layout-flex-3_-_-webkit-flex: 3;
+  --layout-flex-3_-_flex: 3;
+  ;
+
+  --layout-flex-4_-_-ms-flex: 4;
+  --layout-flex-4_-_-webkit-flex: 4;
+  --layout-flex-4_-_flex: 4;
+  ;
+
+  --layout-flex-5_-_-ms-flex: 5;
+  --layout-flex-5_-_-webkit-flex: 5;
+  --layout-flex-5_-_flex: 5;
+  ;
+
+  --layout-flex-6_-_-ms-flex: 6;
+  --layout-flex-6_-_-webkit-flex: 6;
+  --layout-flex-6_-_flex: 6;
+  ;
+
+  --layout-flex-7_-_-ms-flex: 7;
+  --layout-flex-7_-_-webkit-flex: 7;
+  --layout-flex-7_-_flex: 7;
+  ;
+
+  --layout-flex-8_-_-ms-flex: 8;
+  --layout-flex-8_-_-webkit-flex: 8;
+  --layout-flex-8_-_flex: 8;
+  ;
+
+  --layout-flex-9_-_-ms-flex: 9;
+  --layout-flex-9_-_-webkit-flex: 9;
+  --layout-flex-9_-_flex: 9;
+  ;
+
+  --layout-flex-10_-_-ms-flex: 10;
+  --layout-flex-10_-_-webkit-flex: 10;
+  --layout-flex-10_-_flex: 10;
+  ;
+
+  --layout-flex-11_-_-ms-flex: 11;
+  --layout-flex-11_-_-webkit-flex: 11;
+  --layout-flex-11_-_flex: 11;
+  ;
+
+  --layout-flex-12_-_-ms-flex: 12;
+  --layout-flex-12_-_-webkit-flex: 12;
+  --layout-flex-12_-_flex: 12;
+  ;
+
+
+
+  --layout-start_-_-ms-flex-align: start;
+  --layout-start_-_-webkit-align-items: flex-start;
+  --layout-start_-_align-items: flex-start;
+  ;
+
+  --layout-center_-_-ms-flex-align: center;
+  --layout-center_-_-webkit-align-items: center;
+  --layout-center_-_align-items: center;
+  ;
+
+  --layout-end_-_-ms-flex-align: end;
+  --layout-end_-_-webkit-align-items: flex-end;
+  --layout-end_-_align-items: flex-end;
+  ;
+
+  --layout-baseline_-_-ms-flex-align: baseline;
+  --layout-baseline_-_-webkit-align-items: baseline;
+  --layout-baseline_-_align-items: baseline;
+  ;
+
+
+
+  --layout-start-justified_-_-ms-flex-pack: start;
+  --layout-start-justified_-_-webkit-justify-content: flex-start;
+  --layout-start-justified_-_justify-content: flex-start;
+  ;
+
+  --layout-center-justified_-_-ms-flex-pack: center;
+  --layout-center-justified_-_-webkit-justify-content: center;
+  --layout-center-justified_-_justify-content: center;
+  ;
+
+  --layout-end-justified_-_-ms-flex-pack: end;
+  --layout-end-justified_-_-webkit-justify-content: flex-end;
+  --layout-end-justified_-_justify-content: flex-end;
+  ;
+
+  --layout-around-justified_-_-ms-flex-pack: distribute;
+  --layout-around-justified_-_-webkit-justify-content: space-around;
+  --layout-around-justified_-_justify-content: space-around;
+  ;
+
+  --layout-justified_-_-ms-flex-pack: justify;
+  --layout-justified_-_-webkit-justify-content: space-between;
+  --layout-justified_-_justify-content: space-between;
+  ;
+
+  --layout-center-center_-_-ms-flex-align: var(--layout-center_-_-ms-flex-align);
+  --layout-center-center_-_-webkit-align-items: var(--layout-center_-_-webkit-align-items);
+  --layout-center-center_-_align-items: var(--layout-center_-_align-items);
+  --layout-center-center_-_-ms-flex-pack: var(--layout-center-justified_-_-ms-flex-pack);
+  --layout-center-center_-_-webkit-justify-content: var(--layout-center-justified_-_-webkit-justify-content);
+  --layout-center-center_-_justify-content: var(--layout-center-justified_-_justify-content);
+  ;
+
+
+
+  --layout-self-start_-_-ms-align-self: flex-start;
+  --layout-self-start_-_-webkit-align-self: flex-start;
+  --layout-self-start_-_align-self: flex-start;
+  ;
+
+  --layout-self-center_-_-ms-align-self: center;
+  --layout-self-center_-_-webkit-align-self: center;
+  --layout-self-center_-_align-self: center;
+  ;
+
+  --layout-self-end_-_-ms-align-self: flex-end;
+  --layout-self-end_-_-webkit-align-self: flex-end;
+  --layout-self-end_-_align-self: flex-end;
+  ;
+
+  --layout-self-stretch_-_-ms-align-self: stretch;
+  --layout-self-stretch_-_-webkit-align-self: stretch;
+  --layout-self-stretch_-_align-self: stretch;
+  ;
+
+  --layout-self-baseline_-_-ms-align-self: baseline;
+  --layout-self-baseline_-_-webkit-align-self: baseline;
+  --layout-self-baseline_-_align-self: baseline;
+  ;
+
+
+
+  --layout-start-aligned_-_-ms-flex-line-pack: start;
+  --layout-start-aligned_-_-ms-align-content: flex-start;
+  --layout-start-aligned_-_-webkit-align-content: flex-start;
+  --layout-start-aligned_-_align-content: flex-start;
+  ;
+
+  --layout-end-aligned_-_-ms-flex-line-pack: end;
+  --layout-end-aligned_-_-ms-align-content: flex-end;
+  --layout-end-aligned_-_-webkit-align-content: flex-end;
+  --layout-end-aligned_-_align-content: flex-end;
+  ;
+
+  --layout-center-aligned_-_-ms-flex-line-pack: center;
+  --layout-center-aligned_-_-ms-align-content: center;
+  --layout-center-aligned_-_-webkit-align-content: center;
+  --layout-center-aligned_-_align-content: center;
+  ;
+
+  --layout-between-aligned_-_-ms-flex-line-pack: justify;
+  --layout-between-aligned_-_-ms-align-content: space-between;
+  --layout-between-aligned_-_-webkit-align-content: space-between;
+  --layout-between-aligned_-_align-content: space-between;
+  ;
+
+  --layout-around-aligned_-_-ms-flex-line-pack: distribute;
+  --layout-around-aligned_-_-ms-align-content: space-around;
+  --layout-around-aligned_-_-webkit-align-content: space-around;
+  --layout-around-aligned_-_align-content: space-around;
+  ;
+
+
+
+  --layout-block_-_display: block;
+  ;
+
+  --layout-invisible_-_visibility: hidden !important;
+  ;
+
+  --layout-relative_-_position: relative;
+  ;
+
+  --layout-fit_-_position: absolute;
+  --layout-fit_-_top: 0;
+  --layout-fit_-_right: 0;
+  --layout-fit_-_bottom: 0;
+  --layout-fit_-_left: 0;
+  ;
+
+  --layout-scroll_-_-webkit-overflow-scrolling: touch;
+  --layout-scroll_-_overflow: auto;
+  ;
+
+  --layout-fullbleed_-_margin: 0;
+  --layout-fullbleed_-_height: 100vh;
+  ;
+
+
+
+  --layout-fixed-top_-_position: fixed;
+  --layout-fixed-top_-_top: 0;
+  --layout-fixed-top_-_left: 0;
+  --layout-fixed-top_-_right: 0;
+  ;
+
+  --layout-fixed-right_-_position: fixed;
+  --layout-fixed-right_-_top: 0;
+  --layout-fixed-right_-_right: 0;
+  --layout-fixed-right_-_bottom: 0;
+  ;
+
+  --layout-fixed-bottom_-_position: fixed;
+  --layout-fixed-bottom_-_right: 0;
+  --layout-fixed-bottom_-_bottom: 0;
+  --layout-fixed-bottom_-_left: 0;
+  ;
+
+  --layout-fixed-left_-_position: fixed;
+  --layout-fixed-left_-_top: 0;
+  --layout-fixed-left_-_bottom: 0;
+  --layout-fixed-left_-_left: 0;
+  ;
+}
 </style>
 
 <!-- yt-live-chat-app -->
diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js
index 5968f9da..7813c4cb 100644
--- a/frontend/src/lang/en.js
+++ b/frontend/src/lang/en.js
@@ -52,6 +52,7 @@ export default {
     avatarSize: 'Avatar size',
 
     userNames: 'User names',
+    showUserNames: 'Show user names',
     font: 'Font',
     fontSize: 'Font size',
     lineHeight: 'Line height (0 for default)',
diff --git a/frontend/src/lang/ja.js b/frontend/src/lang/ja.js
index 8ce58f57..fcadfb6c 100644
--- a/frontend/src/lang/ja.js
+++ b/frontend/src/lang/ja.js
@@ -52,6 +52,7 @@ export default {
     avatarSize: 'アイコンのサイズ',
 
     userNames: 'ユーザー名',
+    showUserNames: 'ユーザー名を表示する',
     font: 'フォント',
     fontSize: 'フォントサイズ',
     lineHeight: '行の高さ(0はデフォルト)',
diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js
index f9b86bad..819d87bb 100644
--- a/frontend/src/lang/zh.js
+++ b/frontend/src/lang/zh.js
@@ -52,6 +52,7 @@ export default {
     avatarSize: '头像尺寸',
 
     userNames: '用户名',
+    showUserNames: '显示用户名',
     font: '字体',
     fontSize: '字体尺寸',
     lineHeight: '行高(0为默认)',
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index d5a69329..a753a4f7 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -27,8 +27,8 @@ export function formatCurrency (price) {
   }).format(price)
 }
 
-export function getTimeTextMinSec (date) {
+export function getTimeTextHourMin (date) {
+  let hour = date.getHours()
   let min = ('00' + date.getMinutes()).slice(-2)
-  let sec = ('00' + date.getSeconds()).slice(-2)
-  return `${min}:${sec}`
+  return `${hour}:${min}`
 }
diff --git a/frontend/src/views/Room.vue b/frontend/src/views/Room.vue
index b5204db5..77f35aeb 100644
--- a/frontend/src/views/Room.vue
+++ b/frontend/src/views/Room.vue
@@ -188,8 +188,8 @@ export default {
           avatarUrl: data.avatarUrl,
           time: new Date(data.timestamp * 1000),
           authorName: data.authorName,
-          title: 'NEW MEMBER!',
-          content: `Welcome ${data.authorName}!`
+          privilegeType: data.privilegeType,
+          title: 'New member'
         }
         break
       case COMMAND_ADD_SUPER_CHAT:
diff --git a/frontend/src/views/StyleGenerator/index.vue b/frontend/src/views/StyleGenerator/index.vue
index 1973a608..54d029b0 100644
--- a/frontend/src/views/StyleGenerator/index.vue
+++ b/frontend/src/views/StyleGenerator/index.vue
@@ -22,6 +22,9 @@
         </el-form-item>
 
         <h3>{{$t('stylegen.userNames')}}</h3>
+        <el-form-item :label="$t('stylegen.showUserNames')">
+          <el-switch v-model="form.showUserNames"></el-switch>
+        </el-form-item>
         <el-form-item :label="$t('stylegen.font')">
           <el-autocomplete v-model="form.userNameFont" :fetch-suggestions="getFontSuggestions"></el-autocomplete>
         </el-form-item>
@@ -219,15 +222,15 @@ let textMessageTemplate = {
   repeated: 1,
   translation: ''
 }
-let legacyPaidMessageTemplate = {
+let membershipItemTemplate = {
   id: 0,
   addTime: time,
   type: constants.MESSAGE_TYPE_MEMBER,
   avatarUrl: 'https://static.hdslb.com/images/member/noface.gif',
   time: time,
   authorName: '',
-  title: 'NEW MEMBER!',
-  content: ''
+  privilegeType: 3,
+  title: 'New member'
 }
 let paidMessageTemplate = {
   id: 0,
@@ -266,15 +269,14 @@ const EXAMPLE_MESSAGES = [
     content: 'kksk'
   },
   {
-    ...legacyPaidMessageTemplate,
+    ...membershipItemTemplate,
     id: (nextId++).toString(),
-    authorName: '进击的冰糖',
-    content: 'Welcome 进击的冰糖!'
+    authorName: '艾米亚official'
   },
   {
     ...paidMessageTemplate,
     id: (nextId++).toString(),
-    authorName: '无火的残渣',
+    authorName: '愛里紗メイプル',
     price: 66600,
     content: 'Sent 小电视飞船x100'
   },
@@ -288,7 +290,7 @@ const EXAMPLE_MESSAGES = [
   {
     ...paidMessageTemplate,
     id: (nextId++).toString(),
-    authorName: '夏色祭保護協会会長',
+    authorName: 'AstralisUP',
     price: 30,
     content: '言いたいことがあるんだよ!'
   }
diff --git a/frontend/src/views/StyleGenerator/stylegen.js b/frontend/src/views/StyleGenerator/stylegen.js
index dff4cb13..5e96ab03 100644
--- a/frontend/src/views/StyleGenerator/stylegen.js
+++ b/frontend/src/views/StyleGenerator/stylegen.js
@@ -9,6 +9,7 @@ export const DEFAULT_CONFIG = {
   showAvatars: true,
   avatarSize: 24,
 
+  showUserNames: true,
   userNameFont: 'Changa One',
   userNameFontSize: 20,
   userNameLineHeight: 0,
@@ -109,11 +110,11 @@ yt-live-chat-renderer * {
   ${getShowOutlinesStyle(config)}
   font-family: "${config.messageFont}"${FALLBACK_FONTS};
   font-size: ${config.messageFontSize}px !important;
-  line-height: ${config.messageLineHeight}px !important;
+  line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
 }
 
 yt-live-chat-text-message-renderer #content,
-yt-live-chat-legacy-paid-message-renderer #content {
+yt-live-chat-membership-item-renderer #content {
   overflow: initial !important;
 }
 
@@ -133,12 +134,7 @@ yt-live-chat-message-input-renderer {
 }
 
 /* Reduce side padding. */
-yt-live-chat-text-message-renderer,
-yt-live-chat-legacy-paid-message-renderer {
-  ${getPaddingStyle(config)}
-}
-
-yt-live-chat-paid-message-renderer #header {
+yt-live-chat-text-message-renderer {
   ${getPaddingStyle(config)}
 }
 
@@ -147,8 +143,8 @@ yt-live-chat-text-message-renderer #author-photo,
 yt-live-chat-text-message-renderer #author-photo img,
 yt-live-chat-paid-message-renderer #author-photo,
 yt-live-chat-paid-message-renderer #author-photo img,
-yt-live-chat-legacy-paid-message-renderer #author-photo,
-yt-live-chat-legacy-paid-message-renderer #author-photo img {
+yt-live-chat-membership-item-renderer #author-photo,
+yt-live-chat-membership-item-renderer #author-photo img {
   ${config.showAvatars ? '' : 'display: none !important;'}
   width: ${config.avatarSize}px !important;
   height: ${config.avatarSize}px !important;
@@ -189,6 +185,7 @@ yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="memb
 
 /* Channel names. */
 yt-live-chat-text-message-renderer #author-name {
+  ${config.showUserNames ? '' : 'display: none !important;'}
   ${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
   font-family: "${config.userNameFont}"${FALLBACK_FONTS};
   font-size: ${config.userNameFontSize}px !important;
@@ -213,8 +210,8 @@ ${!config.messageOnNewLine ? '' : `yt-live-chat-text-message-renderer #message {
 /* SuperChat/Fan Funding Messages. */
 yt-live-chat-paid-message-renderer #author-name,
 yt-live-chat-paid-message-renderer #author-name *,
-yt-live-chat-legacy-paid-message-renderer #event-text,
-yt-live-chat-legacy-paid-message-renderer #event-text * {
+yt-live-chat-membership-item-renderer #header-content-inner-column,
+yt-live-chat-membership-item-renderer #header-content-inner-column * {
   ${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
   font-family: "${config.firstLineFont}"${FALLBACK_FONTS};
   font-size: ${config.firstLineFontSize}px !important;
@@ -223,8 +220,8 @@ yt-live-chat-legacy-paid-message-renderer #event-text * {
 
 yt-live-chat-paid-message-renderer #purchase-amount,
 yt-live-chat-paid-message-renderer #purchase-amount *,
-yt-live-chat-legacy-paid-message-renderer #detail-text,
-yt-live-chat-legacy-paid-message-renderer #detail-text * {
+yt-live-chat-membership-item-renderer #header-subtext,
+yt-live-chat-membership-item-renderer #header-subtext * {
   ${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
   font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
   font-size: ${config.secondLineFontSize}px !important;
@@ -243,17 +240,18 @@ yt-live-chat-paid-message-renderer {
   margin: 4px 0 !important;
 }
 
-yt-live-chat-legacy-paid-message-renderer #card {
+yt-live-chat-membership-item-renderer #card,
+yt-live-chat-membership-item-renderer #header {
   ${getShowNewMemberBgStyle(config)}
 }
 
 yt-live-chat-text-message-renderer a,
-yt-live-chat-legacy-paid-message-renderer a {
+yt-live-chat-membership-item-renderer a {
   text-decoration: none !important;
 }
 
 yt-live-chat-text-message-renderer[is-deleted],
-yt-live-chat-legacy-paid-message-renderer[is-deleted] {
+yt-live-chat-membership-item-renderer[is-deleted] {
   display: none !important;
 }
 
@@ -399,7 +397,8 @@ ${keyframes.join('\n')}
 }
 
 yt-live-chat-text-message-renderer,
-yt-live-chat-legacy-paid-message-renderer {
+yt-live-chat-membership-item-renderer,
+yt-live-chat-paid-message-renderer {
   animation: anim ${totalTime}ms;
   animation-fill-mode: both;
 }`

From 602a9f2a441210c488633660e411b0c2ab12abf0 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 19 Jul 2020 22:19:05 +0800
Subject: [PATCH 03/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=9D=E5=A7=8B?=
 =?UTF-8?q?=E5=8C=96=E6=88=BF=E9=97=B4API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 blivedm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/blivedm b/blivedm
index d173228c..6618bf5c 160000
--- a/blivedm
+++ b/blivedm
@@ -1 +1 @@
-Subproject commit d173228c5f83c2f5f94551259e0e6c01e929d92c
+Subproject commit 6618bf5c484cbe5a6f7e32f8e5d65ed0b74849c7

From 96070ac473ba8e41c3f3e50db6b80b3222798fc8 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 16 Aug 2020 10:10:49 +0800
Subject: [PATCH 04/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BD=BF=E7=94=A8?=
 =?UTF-8?q?=E6=96=B9=E6=B3=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 76 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 3d12a0f2..9877b7d1 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
 * 支持自动翻译弹幕、醒目留言到日语
 
 ## 使用方法
-### 本地使用
+### 一、本地使用
 1. 下载[发布版](https://github.com/xfgryujk/blivechat/releases)(仅提供x64 Windows版)
 2. 双击`blivechat.exe`运行服务器,或者用命令行可以指定host和端口号:
    ```bat
@@ -32,14 +32,22 @@
 * 本地使用时不要关闭blivechat.exe那个黑框,否则不能继续获取弹幕
 * 样式生成器没有列出所有本地字体,但是可以手动输入本地字体
 
-### 公共服务器
-请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能出现直播事故
+### 二、公共服务器
+请优先在本地使用,使用公共服务器会有更大的弹幕延迟,而且服务器故障时可能发生直播事故
 
 * [第三方公共服务器](http://chat.bilisc.com/)
 * [仅样式生成器](https://style.vtbs.moe/)
 
-### 源代码版
-1. 编译前端(需要安装Node.js和npm):
+### 三、源代码版(自建服务器或在Windows以外平台)
+0. 由于使用了git子模块,clone时需要加上`--recursive`参数:
+   ```sh
+   git clone --recursive https://github.com/xfgryujk/blivechat.git
+   ```
+   如果已经clone,拉子模块的方法:
+   ```sh
+   git submodule update --init --recursive
+   ```
+1. 编译前端(需要安装Node.js):
    ```sh
    cd frontend
    npm i
@@ -56,8 +64,70 @@
    ```
 3. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
 
-### Docker
+### 四、Docker(自建服务器)
 1. ```sh
    docker run -d -p 12450:12450 xfgryujk/blivechat:latest
    ```
 2. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
+
+### nginx配置(可选)
+自建服务器时使用,`sudo vim /etc/nginx/sites-enabled/blivechat.conf`
+
+```conf
+upstream blivechat {
+	# blivechat地址
+	server 127.0.0.1:12450;
+}
+
+# 强制HTTPS
+server {
+	listen 80;
+	listen [::]:80;
+	server_name YOUR.DOMAIN.NAME;
+
+	return 301 https://$server_name$request_uri;
+}
+
+server {
+	listen 443 ssl;
+	listen [::]:443 ssl;
+	server_name YOUR.DOMAIN.NAME;
+
+	# SSL
+	ssl_certificate /PATH/TO/CERT.crt;
+	ssl_certificate_key /PATH/TO/CERT_KEY.key;
+	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
+	ssl_prefer_server_ciphers on;
+
+	# 代理header
+	proxy_set_header Host $host;
+	proxy_set_header X-Real-IP $remote_addr;
+	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+	# 静态文件
+	location / {
+		root /PATH/TO/BLIVECHAT/frontend/dist;
+		# 如果文件不存在,交给前端路由
+		try_files $uri $uri/ /index.html;
+	}
+	# 动态API
+	location = /server_info {
+		proxy_pass http://blivechat;
+	}
+	# websocket
+	location = /chat {
+		proxy_pass http://blivechat;
+
+		# 代理websocket必须设置
+		proxy_http_version 1.1;
+		proxy_set_header Upgrade $http_upgrade;
+		proxy_set_header Connection "Upgrade";
+		
+		# 由于这个块有proxy_set_header,这些不会自动继承
+		proxy_set_header Host $host;
+		proxy_set_header X-Real-IP $remote_addr;
+		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+	}
+}
+```

From 127344802d4b0a2ec995c71dc736f0929e2f829b Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 16 Aug 2020 12:13:53 +0800
Subject: [PATCH 05/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=81=E8=AE=B8?=
 =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=BF=BB=E8=AF=91=E7=9A=84=E6=88=BF=E9=97=B4?=
 =?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/chat.py     | 132 ++++++++++++++++++++++++++++++------------------
 config.py       |  21 ++++++--
 data/config.ini |   4 ++
 3 files changed, 103 insertions(+), 54 deletions(-)

diff --git a/api/chat.py b/api/chat.py
index 6dd152af..befab3b2 100644
--- a/api/chat.py
+++ b/api/chat.py
@@ -149,34 +149,21 @@ async def __on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
 
         id_ = uuid.uuid4().hex
         # 为了节省带宽用list而不是dict
-        self.send_message(Command.ADD_TEXT, [
-            # 0: avatarUrl
+        self.send_message(Command.ADD_TEXT, make_text_message(
             await models.avatar.get_avatar_url(danmaku.uid),
-            # 1: timestamp
             int(danmaku.timestamp / 1000),
-            # 2: authorName
             danmaku.uname,
-            # 3: authorType
             author_type,
-            # 4: content
             danmaku.msg,
-            # 5: privilegeType
             danmaku.privilege_type,
-            # 6: isGiftDanmaku
-            1 if danmaku.msg_type else 0,
-            # 7: authorLevel
+            danmaku.msg_type,
             danmaku.user_level,
-            # 8: isNewbie
-            1 if danmaku.urank < 10000 else 0,
-            # 9: isMobileVerified
-            1 if danmaku.mobile_verify else 0,
-            # 10: medalLevel
+            danmaku.urank < 10000,
+            danmaku.mobile_verify,
             0 if danmaku.room_id != self.room_id else danmaku.medal_level,
-            # 11: id
             id_,
-            # 12: translation
             translation
-        ])
+        ))
 
         if need_translate:
             await self._translate_and_response(danmaku.msg, id_)
@@ -245,8 +232,10 @@ async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
         })
 
     def _need_translate(self, text):
+        cfg = config.get_config()
         return (
-            config.get_config().enable_translate
+            cfg.enable_translate
+            and (not cfg.allow_translate_rooms or self.room_id in cfg.allow_translate_rooms)
             and self.auto_translate_count > 0
             and models.translate.need_translate(text)
         )
@@ -267,6 +256,39 @@ async def _translate_and_response(self, text, msg_id):
         )
 
 
+def make_text_message(avatar_url, timestamp, author_name, author_type, content, privilege_type,
+                      is_gift_danmaku, author_level, is_newbie, is_mobile_verified, medal_level,
+                      id_, translation):
+    return [
+        # 0: avatarUrl
+        avatar_url,
+        # 1: timestamp
+        timestamp,
+        # 2: authorName
+        author_name,
+        # 3: authorType
+        author_type,
+        # 4: content
+        content,
+        # 5: privilegeType
+        privilege_type,
+        # 6: isGiftDanmaku
+        1 if is_gift_danmaku else 0,
+        # 7: authorLevel
+        author_level,
+        # 8: isNewbie
+        1 if is_newbie else 0,
+        # 9: isMobileVerified
+        1 if is_mobile_verified else 0,
+        # 10: medalLevel
+        medal_level,
+        # 11: id
+        id_,
+        # 12: translation
+        translation
+    ]
+
+
 class RoomManager:
     def __init__(self):
         self._rooms: Dict[int, Room] = {}
@@ -285,8 +307,7 @@ async def add_client(self, room_id, client: 'ChatHandler'):
         if client.auto_translate:
             room.auto_translate_count += 1
 
-        if client.application.settings['debug']:
-            await client.send_test_message()
+        await client.on_join_room()
 
     def del_client(self, room_id, client: 'ChatHandler'):
         room = self._rooms.get(room_id, None)
@@ -391,6 +412,41 @@ def check_origin(self, origin):
             return True
         return super().check_origin(origin)
 
+    @property
+    def has_joined_room(self):
+        return self.room_id is not None
+
+    def send_message(self, cmd, data):
+        body = json.dumps({'cmd': cmd, 'data': data})
+        try:
+            self.write_message(body)
+        except tornado.websocket.WebSocketClosedError:
+            self.on_close()
+
+    async def on_join_room(self):
+        if self.application.settings['debug']:
+            await self.send_test_message()
+
+        # 不允许自动翻译的提示
+        if self.auto_translate:
+            cfg = config.get_config()
+            if cfg.allow_translate_rooms and self.room_id not in cfg.allow_translate_rooms:
+                self.send_message(Command.ADD_TEXT, make_text_message(
+                    models.avatar.DEFAULT_AVATAR_URL,
+                    int(time.time()),
+                    'blivechat',
+                    2,
+                    'Translation is not allowed in this room, please download to use translation',
+                    0,
+                    False,
+                    60,
+                    False,
+                    True,
+                    0,
+                    uuid.uuid4().hex,
+                    ''
+                ))
+
     # 测试用
     async def send_test_message(self):
         base_data = {
@@ -398,34 +454,21 @@ async def send_test_message(self):
             'timestamp': int(time.time()),
             'authorName': 'xfgryujk',
         }
-        text_data = [
-            # 0: avatarUrl
+        text_data = make_text_message(
             base_data['avatarUrl'],
-            # 1: timestamp
             base_data['timestamp'],
-            # 2: authorName
             base_data['authorName'],
-            # 3: authorType
             0,
-            # 4: content
             '我能吞下玻璃而不伤身体',
-            # 5: privilegeType
-            0,
-            # 6: isGiftDanmaku
             0,
-            # 7: authorLevel
+            False,
             20,
-            # 8: isNewbie
+            False,
+            True,
             0,
-            # 9: isMobileVerified
-            1,
-            # 10: medalLevel
-            0,
-            # 11: id
             uuid.uuid4().hex,
-            # 12: translation
             ''
-        ]
+        )
         member_data = {
             **base_data,
             'id': uuid.uuid4().hex,
@@ -463,14 +506,3 @@ async def send_test_message(self):
         gift_data['totalCoin'] = 1245000
         gift_data['giftName'] = '小电视飞船'
         self.send_message(Command.ADD_GIFT, gift_data)
-
-    @property
-    def has_joined_room(self):
-        return self.room_id is not None
-
-    def send_message(self, cmd, data):
-        body = json.dumps({'cmd': cmd, 'data': data})
-        try:
-            self.write_message(body)
-        except tornado.websocket.WebSocketClosedError:
-            self.on_close()
diff --git a/config.py b/config.py
index 47f348fd..3c540f3d 100644
--- a/config.py
+++ b/config.py
@@ -13,14 +13,20 @@
 
 
 def init():
-    reload()
+    if reload():
+        return
+    logger.warning('Using default config')
+    global _config
+    _config = AppConfig()
 
 
 def reload():
     config = AppConfig()
-    if config.load(CONFIG_PATH):
-        global _config
-        _config = config
+    if not config.load(CONFIG_PATH):
+        return False
+    global _config
+    _config = config
+    return True
 
 
 def get_config():
@@ -31,6 +37,7 @@ class AppConfig:
     def __init__(self):
         self.database_url = 'sqlite:///data/database.db'
         self.enable_translate = True
+        self.allow_translate_rooms = {}
 
     def load(self, path):
         config = configparser.ConfigParser()
@@ -39,6 +46,12 @@ def load(self, path):
             app_section = config['app']
             self.database_url = app_section['database_url']
             self.enable_translate = app_section.getboolean('enable_translate')
+            allow_translate_rooms = app_section['allow_translate_rooms'].strip()
+            if allow_translate_rooms == '':
+                self.allow_translate_rooms = {}
+            else:
+                allow_translate_rooms = allow_translate_rooms.split(',')
+                self.allow_translate_rooms = set(map(lambda id_: int(id_.strip()), allow_translate_rooms))
         except (KeyError, ValueError):
             logger.exception('Failed to load config:')
             return False
diff --git a/data/config.ini b/data/config.ini
index 8e6edd67..3091bcac 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -3,9 +3,13 @@
 database_url = sqlite:///data/database.db
 # Enable auto translate to Japanese
 enable_translate = true
+# Comma separated room IDs in which translation are not allowed. If empty, all are allowed
+# Example: allow_translate_rooms = 4895312,22347054,21693691
+allow_translate_rooms =
 
 
 # DON'T modify this section
 [DEFAULT]
 database_url = sqlite:///data/database.db
 enable_translate = true
+allow_translate_rooms =

From d25a32beafba4b567a4c9cd3523be7d12a3399cc Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 16 Aug 2020 12:15:01 +0800
Subject: [PATCH 06/18] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=89=8C=E5=AD=90?=
 =?UTF-8?q?=E7=AD=89=E7=BA=A7=E4=B8=8A=E9=99=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 frontend/src/views/Home.vue | 2 +-
 models/translate.py         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index 23c57918..a05496bc 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -53,7 +53,7 @@
           <el-input v-model="form.blockUsers" type="textarea" :rows="5" :placeholder="$t('home.onePerLine')"></el-input>
         </el-form-item>
         <el-form-item :label="$t('home.blockMedalLevel')">
-          <el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="20"></el-slider>
+          <el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="40"></el-slider>
         </el-form-item>
       </el-tab-pane>
 
diff --git a/models/translate.py b/models/translate.py
index 205e0d0f..19d340b2 100644
--- a/models/translate.py
+++ b/models/translate.py
@@ -17,7 +17,7 @@
 
 NO_TRANSLATE_TEXTS = {
     '草', '草草', '草草草', '草生', '大草原', '上手', '上手上手', '理解', '理解理解', '天才', '天才天才',
-    '强', '余裕', '余裕余裕', '大丈夫', '再放送', '放送事故'
+    '强', '余裕', '余裕余裕', '大丈夫', '再放送', '放送事故', '清楚', '清楚清楚'
 }
 
 _main_event_loop = asyncio.get_event_loop()

From bfa605489909864a9ada9447f47e8179f265439c Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Tue, 18 Aug 2020 21:48:33 +0800
Subject: [PATCH 07/18] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=9A=84=E6=88=BF?=
 =?UTF-8?q?=E9=97=B4URL=E6=94=B9=E6=88=90=E5=8A=A0=E8=BD=BD=E5=99=A8URL?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/main.py                 | 12 +++++++++---
 frontend/src/views/Home.vue | 10 +++++++++-
 main.py                     |  4 +---
 3 files changed, 19 insertions(+), 7 deletions(-)

diff --git a/api/main.py b/api/main.py
index 1846619f..bbe2716a 100644
--- a/api/main.py
+++ b/api/main.py
@@ -8,9 +8,15 @@
 
 
 class MainHandler(tornado.web.StaticFileHandler):
-    """为了使用Vue Router的history模式,把所有请求转发到index.html"""
-    async def get(self, *args, **kwargs):
-        await super().get('index.html', *args, **kwargs)
+    """为了使用Vue Router的history模式,把不存在的文件请求转发到index.html"""
+    async def get(self, path, include_body=True):
+        try:
+            await super().get(path, include_body)
+        except tornado.web.HTTPError as e:
+            if e.status_code != 404:
+                raise
+            # 不存在的文件请求转发到index.html,交给前端路由
+            await super().get('index.html', include_body)
 
 
 # noinspection PyAbstractClass
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index a05496bc..c31b9e08 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -66,7 +66,7 @@
 
     <el-divider></el-divider>
     <el-form-item :label="$t('home.roomUrl')">
-      <el-input ref="roomUrlInput" readonly :value="roomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
+      <el-input ref="roomUrlInput" readonly :value="loaderUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
       <el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
     </el-form-item>
     <el-form-item>
@@ -107,6 +107,14 @@ export default {
       delete query.roomId
       let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}, query})
       return `${window.location.protocol}//${window.location.host}${resolved.href}`
+    },
+    loaderUrl() {
+      if (this.roomUrl === '') {
+        return ''
+      }
+      let url = new URL('https://xfgryujk.sinacloud.net/blivechat/loader.html')
+      url.searchParams.append('url', this.roomUrl)
+      return url.href
     }
   },
   watch: {
diff --git a/main.py b/main.py
index ad7db0ea..1190e6ba 100644
--- a/main.py
+++ b/main.py
@@ -24,9 +24,7 @@
     (r'/server_info', api.main.ServerInfoHandler),
     (r'/chat', api.chat.ChatHandler),
 
-    (r'/((css|fonts|img|js|static)/.*)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
-    (r'/(favicon\.ico)', tornado.web.StaticFileHandler, {'path': WEB_ROOT}),
-    (r'/.*', api.main.MainHandler, {'path': WEB_ROOT})
+    (r'/(.*)', api.main.MainHandler, {'path': WEB_ROOT, 'default_filename': 'index.html'})
 ]
 
 

From 4e7cc7a782b4b085fe9735a5f4ef2d9d8e22051e Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Wed, 19 Aug 2020 22:55:54 +0800
Subject: [PATCH 08/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0nginx=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 9877b7d1..60a50fe5 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,6 @@
 
 **注意事项:**
 
-* 应该先启动blivechat后启动OBS,否则网页会加载失败,这时应该刷新OBS的浏览器源,显示Loaded则加载成功
 * 本地使用时不要关闭blivechat.exe那个黑框,否则不能继续获取弹幕
 * 样式生成器没有列出所有本地字体,但是可以手动输入本地字体
 
@@ -75,6 +74,7 @@
 
 ```conf
 upstream blivechat {
+	keepalive 8;
 	# blivechat地址
 	server 127.0.0.1:12450;
 }
@@ -96,12 +96,11 @@ server {
 	# SSL
 	ssl_certificate /PATH/TO/CERT.crt;
 	ssl_certificate_key /PATH/TO/CERT_KEY.key;
-	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
-	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
-	ssl_prefer_server_ciphers on;
 
 	# 代理header
+	proxy_http_version 1.1;
 	proxy_set_header Host $host;
+	proxy_set_header Connection "";
 	proxy_set_header X-Real-IP $remote_addr;
 	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
@@ -120,10 +119,9 @@ server {
 		proxy_pass http://blivechat;
 
 		# 代理websocket必须设置
-		proxy_http_version 1.1;
 		proxy_set_header Upgrade $http_upgrade;
 		proxy_set_header Connection "Upgrade";
-		
+
 		# 由于这个块有proxy_set_header,这些不会自动继承
 		proxy_set_header Host $host;
 		proxy_set_header X-Real-IP $remote_addr;

From 7cfa94c68c761874762f3fa0a9eb785631b1fff2 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Wed, 19 Aug 2020 23:16:01 +0800
Subject: [PATCH 09/18] =?UTF-8?q?=E6=94=B9API=E8=B7=AF=E5=BE=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                   | 4 ++--
 frontend/src/views/Home.vue | 2 +-
 frontend/src/views/Room.vue | 2 +-
 main.py                     | 4 ++++
 4 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 60a50fe5..987d8be0 100644
--- a/README.md
+++ b/README.md
@@ -111,11 +111,11 @@ server {
 		try_files $uri $uri/ /index.html;
 	}
 	# 动态API
-	location = /server_info {
+	location /api {
 		proxy_pass http://blivechat;
 	}
 	# websocket
-	location = /chat {
+	location = /api/chat {
 		proxy_pass http://blivechat;
 
 		# 代理websocket必须设置
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index c31b9e08..a6a91606 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -129,7 +129,7 @@ export default {
   methods: {
     async updateServerConfig() {
       try {
-        this.serverConfig = (await axios.get(`/server_info`)).data.config
+        this.serverConfig = (await axios.get('/api/server_info')).data.config
       } catch (e) {
         this.$message.error('Failed to fetch server information: ' + e)
       }
diff --git a/frontend/src/views/Room.vue b/frontend/src/views/Room.vue
index 77f35aeb..f31c7d72 100644
--- a/frontend/src/views/Room.vue
+++ b/frontend/src/views/Room.vue
@@ -84,7 +84,7 @@ export default {
       const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
       // 开发时使用localhost:12450
       const host = process.env.NODE_ENV === 'development' ? 'localhost:12450' : window.location.host
-      const url = `${protocol}://${host}/chat`
+      const url = `${protocol}://${host}/api/chat`
       this.websocket = new WebSocket(url)
       this.websocket.onopen = this.onWsOpen
       this.websocket.onclose = this.onWsClose
diff --git a/main.py b/main.py
index 1190e6ba..190544a2 100644
--- a/main.py
+++ b/main.py
@@ -21,6 +21,10 @@
 WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
 
 routes = [
+    (r'/api/server_info', api.main.ServerInfoHandler),
+    (r'/api/chat', api.chat.ChatHandler),
+
+    # TODO 兼容旧版,下版本移除
     (r'/server_info', api.main.ServerInfoHandler),
     (r'/chat', api.chat.ChatHandler),
 

From 38774252cca28338f67beab63557e219e8b6631d Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Wed, 19 Aug 2020 23:17:29 +0800
Subject: [PATCH 10/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0blivedm?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 blivedm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/blivedm b/blivedm
index 6618bf5c..4c64c1bd 160000
--- a/blivedm
+++ b/blivedm
@@ -1 +1 @@
-Subproject commit 6618bf5c484cbe5a6f7e32f8e5d65ed0b74849c7
+Subproject commit 4c64c1bd1e9fe634894d7b781eab1fef0e753907

From 1252e1942d899f3de5db905cecd77129ebeb2ae5 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 22 Aug 2020 18:38:52 +0800
Subject: [PATCH 11/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0tornado=20xheaders?=
 =?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/chat.py     |  2 +-
 config.py       | 10 ++++++++--
 data/config.ini |  6 ++++++
 main.py         |  7 ++++++-
 4 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/api/chat.py b/api/chat.py
index befab3b2..9ea1ad77 100644
--- a/api/chat.py
+++ b/api/chat.py
@@ -436,7 +436,7 @@ async def on_join_room(self):
                     int(time.time()),
                     'blivechat',
                     2,
-                    'Translation is not allowed in this room, please download to use translation',
+                    'Translation is not allowed in this room. Please download to use translation',
                     0,
                     False,
                     60,
diff --git a/config.py b/config.py
index 3c540f3d..2e0f60a0 100644
--- a/config.py
+++ b/config.py
@@ -38,20 +38,26 @@ def __init__(self):
         self.database_url = 'sqlite:///data/database.db'
         self.enable_translate = True
         self.allow_translate_rooms = {}
+        self.tornado_xheaders = False
 
     def load(self, path):
-        config = configparser.ConfigParser()
-        config.read(path)
         try:
+            config = configparser.ConfigParser()
+            config.read(path)
+
             app_section = config['app']
             self.database_url = app_section['database_url']
             self.enable_translate = app_section.getboolean('enable_translate')
+
             allow_translate_rooms = app_section['allow_translate_rooms'].strip()
             if allow_translate_rooms == '':
                 self.allow_translate_rooms = {}
             else:
                 allow_translate_rooms = allow_translate_rooms.split(',')
                 self.allow_translate_rooms = set(map(lambda id_: int(id_.strip()), allow_translate_rooms))
+
+            self.tornado_xheaders = app_section.getboolean('tornado_xheaders')
+
         except (KeyError, ValueError):
             logger.exception('Failed to load config:')
             return False
diff --git a/data/config.ini b/data/config.ini
index 3091bcac..0a4c7040 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -1,15 +1,21 @@
 [app]
 # See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
 database_url = sqlite:///data/database.db
+
 # Enable auto translate to Japanese
 enable_translate = true
+
 # Comma separated room IDs in which translation are not allowed. If empty, all are allowed
 # Example: allow_translate_rooms = 4895312,22347054,21693691
 allow_translate_rooms =
 
+# Set to true if you are using a reverse proxy server such as nginx
+tornado_xheaders = false
+
 
 # DON'T modify this section
 [DEFAULT]
 database_url = sqlite:///data/database.db
 enable_translate = true
 allow_translate_rooms =
+tornado_xheaders = false
diff --git a/main.py b/main.py
index 190544a2..6bb7dad5 100644
--- a/main.py
+++ b/main.py
@@ -70,8 +70,13 @@ def run_server(host, port, debug):
         debug=debug,
         autoreload=False
     )
+    cfg = config.get_config()
     try:
-        app.listen(port, host)
+        app.listen(
+            port,
+            host,
+            xheaders=cfg.tornado_xheaders
+        )
     except OSError:
         logger.warning('Address is used %s:%d', host, port)
         return

From 28b04f27eefeb92d8534feb8c6e55d64546e53bb Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 29 Aug 2020 14:27:15 +0800
Subject: [PATCH 12/18] =?UTF-8?q?=E6=94=B9=E4=B8=80=E4=BA=9B=E9=BB=98?=
 =?UTF-8?q?=E8=AE=A4=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 frontend/src/api/config.js                    | 2 +-
 frontend/src/views/StyleGenerator/stylegen.js | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js
index 96c1c7e8..a4a44a2b 100644
--- a/frontend/src/api/config.js
+++ b/frontend/src/api/config.js
@@ -5,7 +5,7 @@ export const DEFAULT_CONFIG = {
   showDanmaku: true,
   showGift: true,
   showGiftName: false,
-  mergeSimilarDanmaku: true,
+  mergeSimilarDanmaku: false,
   mergeGift: true,
   maxNumber: 60,
 
diff --git a/frontend/src/views/StyleGenerator/stylegen.js b/frontend/src/views/StyleGenerator/stylegen.js
index 5e96ab03..9af34825 100644
--- a/frontend/src/views/StyleGenerator/stylegen.js
+++ b/frontend/src/views/StyleGenerator/stylegen.js
@@ -17,7 +17,7 @@ export const DEFAULT_CONFIG = {
   ownerUserNameColor: '#ffd600',
   moderatorUserNameColor: '#5e84f1',
   memberUserNameColor: '#0f9d58',
-  showBadges: false,
+  showBadges: true,
   showColon: true,
 
   messageFont: 'Imprima',

From 4d58245ad98e9e1615183d4a1befa897e3774960 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sun, 30 Aug 2020 17:46:04 +0800
Subject: [PATCH 13/18] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=A0=E8=BD=BD?=
 =?UTF-8?q?=E5=99=A8URL=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/main.py                 |  3 ++-
 config.py                   |  4 +++-
 data/config.ini             |  4 ++++
 frontend/src/views/Home.vue | 12 ++++++++----
 4 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/api/main.py b/api/main.py
index bbe2716a..a24ef521 100644
--- a/api/main.py
+++ b/api/main.py
@@ -26,6 +26,7 @@ async def get(self):
         self.write({
             'version': update.VERSION,
             'config': {
-                'enableTranslate': cfg.enable_translate
+                'enableTranslate': cfg.enable_translate,
+                'loaderUrl': cfg.loader_url
             }
         })
diff --git a/config.py b/config.py
index 2e0f60a0..ec3dc925 100644
--- a/config.py
+++ b/config.py
@@ -39,6 +39,7 @@ def __init__(self):
         self.enable_translate = True
         self.allow_translate_rooms = {}
         self.tornado_xheaders = False
+        self.loader_url = ''
 
     def load(self, path):
         try:
@@ -49,7 +50,7 @@ def load(self, path):
             self.database_url = app_section['database_url']
             self.enable_translate = app_section.getboolean('enable_translate')
 
-            allow_translate_rooms = app_section['allow_translate_rooms'].strip()
+            allow_translate_rooms = app_section['allow_translate_rooms']
             if allow_translate_rooms == '':
                 self.allow_translate_rooms = {}
             else:
@@ -57,6 +58,7 @@ def load(self, path):
                 self.allow_translate_rooms = set(map(lambda id_: int(id_.strip()), allow_translate_rooms))
 
             self.tornado_xheaders = app_section.getboolean('tornado_xheaders')
+            self.loader_url = app_section['loader_url']
 
         except (KeyError, ValueError):
             logger.exception('Failed to load config:')
diff --git a/data/config.ini b/data/config.ini
index 0a4c7040..b0df8cea 100644
--- a/data/config.ini
+++ b/data/config.ini
@@ -12,6 +12,9 @@ allow_translate_rooms =
 # Set to true if you are using a reverse proxy server such as nginx
 tornado_xheaders = false
 
+# Use a loader so that you can run OBS before blivechat. If empty, no loader is used
+loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
+
 
 # DON'T modify this section
 [DEFAULT]
@@ -19,3 +22,4 @@ database_url = sqlite:///data/database.db
 enable_translate = true
 allow_translate_rooms =
 tornado_xheaders = false
+loader_url =
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index a6a91606..8ce59d3d 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -66,7 +66,7 @@
 
     <el-divider></el-divider>
     <el-form-item :label="$t('home.roomUrl')">
-      <el-input ref="roomUrlInput" readonly :value="loaderUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
+      <el-input ref="roomUrlInput" readonly :value="obsRoomUrl" style="width: calc(100% - 8em); margin-right: 1em;"></el-input>
       <el-button type="primary" @click="copyUrl">{{$t('home.copy')}}</el-button>
     </el-form-item>
     <el-form-item>
@@ -90,7 +90,8 @@ export default {
   data() {
     return {
       serverConfig: {
-        enableTranslate: true
+        enableTranslate: true,
+        loaderUrl: ''
       },
       form: {
         roomId: parseInt(window.localStorage.roomId || '1'),
@@ -108,11 +109,14 @@ export default {
       let resolved = this.$router.resolve({name: 'room', params: {roomId: this.form.roomId}, query})
       return `${window.location.protocol}//${window.location.host}${resolved.href}`
     },
-    loaderUrl() {
+    obsRoomUrl() {
       if (this.roomUrl === '') {
         return ''
       }
-      let url = new URL('https://xfgryujk.sinacloud.net/blivechat/loader.html')
+      if (this.serverConfig.loaderUrl === '') {
+        return this.roomUrl
+      }
+      let url = new URL(this.serverConfig.loaderUrl)
       url.searchParams.append('url', this.roomUrl)
       return url.href
     }

From ca55f9917da8d657825bd2b46255be32421747a6 Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Thu, 3 Sep 2020 20:01:54 +0800
Subject: [PATCH 14/18] =?UTF-8?q?=E6=97=A5=E5=BF=97=E4=BF=9D=E5=AD=98?=
 =?UTF-8?q?=E5=88=B0=E6=96=87=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 log/.gitkeep |  0
 main.py      | 14 +++++++++++---
 2 files changed, 11 insertions(+), 3 deletions(-)
 create mode 100644 log/.gitkeep

diff --git a/log/.gitkeep b/log/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/main.py b/main.py
index 6bb7dad5..d6d2641b 100644
--- a/main.py
+++ b/main.py
@@ -2,6 +2,7 @@
 
 import argparse
 import logging
+import logging.handlers
 import os
 import webbrowser
 
@@ -18,7 +19,9 @@
 
 logger = logging.getLogger(__name__)
 
-WEB_ROOT = os.path.join(os.path.dirname(__file__), 'frontend', 'dist')
+BASE_PATH = os.path.dirname(os.path.realpath(__file__))
+WEB_ROOT = os.path.join(BASE_PATH, 'frontend', 'dist')
+LOG_FILE_NAME = os.path.join(BASE_PATH, 'log', 'blivechat.log')
 
 routes = [
     (r'/api/server_info', api.main.ServerInfoHandler),
@@ -47,7 +50,7 @@ def main():
 
 
 def parse_args():
-    parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播聊天层')
+    parser = argparse.ArgumentParser(description='用于OBS的仿YouTube风格的bilibili直播评论栏')
     parser.add_argument('--host', help='服务器host,默认为127.0.0.1', default='127.0.0.1')
     parser.add_argument('--port', help='服务器端口,默认为12450', type=int, default=12450)
     parser.add_argument('--debug', help='调试模式', action='store_true')
@@ -55,11 +58,16 @@ def parse_args():
 
 
 def init_logging(debug):
+    stream_handler = logging.StreamHandler()
+    file_handler = logging.handlers.TimedRotatingFileHandler(
+        LOG_FILE_NAME, encoding='utf-8', when='midnight', backupCount=7, delay=True
+    )
     logging.basicConfig(
         format='{asctime} {levelname} [{name}]: {message}',
         datefmt='%Y-%m-%d %H:%M:%S',
         style='{',
-        level=logging.INFO if not debug else logging.DEBUG
+        level=logging.INFO if not debug else logging.DEBUG,
+        handlers=[stream_handler, file_handler]
     )
 
 

From 626099f835bbc70682794d357490548c8acde92d Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 5 Sep 2020 08:45:19 +0800
Subject: [PATCH 15/18] =?UTF-8?q?docker=E6=9A=B4=E9=9C=B2=E4=B8=80?=
 =?UTF-8?q?=E4=BA=9B=E7=9B=AE=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .dockerignore |  7 ++++++-
 Dockerfile    | 16 +++++++++-------
 README.md     |  6 +++++-
 3 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index 7549a713..ab6ea41b 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,10 +7,15 @@ build/
 **/node_modules/
 
 # IDEs and editors
-/.idea
+.idea/
 
 # misc
 **/.git*
 *.spec
 screenshots/
 README.md
+
+# runtime data
+data/*
+!data/config.ini
+log/*
diff --git a/Dockerfile b/Dockerfile
index 58889d71..82bc1a22 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,20 +14,22 @@ RUN wget https://nodejs.org/dist/v10.16.0/node-v10.16.0-linux-x64.tar.xz \
     && ln -s /node-v10.16.0-linux-x64/bin/npm /usr/local/bin/npm
 
 # 后端依赖
-COPY requirements.txt /blivechat/
-RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r /blivechat/requirements.txt
+WORKDIR /blivechat
+COPY requirements.txt ./
+RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
 
 # 前端依赖
-WORKDIR /blivechat/frontend
-COPY frontend/package*.json ./
+WORKDIR ./frontend
+COPY frontend/package.json frontend/package-lock.json ./
 RUN npm i --registry=https://registry.npm.taobao.org
 
-# 编译
-COPY . /blivechat
+# 编译前端
+COPY . ../
 RUN npm run build
 
 # 运行
-WORKDIR /blivechat
+WORKDIR ..
+VOLUME /blivechat/data /blivechat/log /blivechat/frontend/dist
 EXPOSE 12450
 ENTRYPOINT ["python3", "main.py"]
 CMD ["--host", "0.0.0.0", "--port", "12450"]
diff --git a/README.md b/README.md
index 987d8be0..7c34d926 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,11 @@
 
 ### 四、Docker(自建服务器)
 1. ```sh
-   docker run -d -p 12450:12450 xfgryujk/blivechat:latest
+   docker run --name blivechat -d -p 12450:12450 \
+     --mount source=blc-data,target=/blivechat/data \
+     --mount source=blc-log,target=/blivechat/log \
+     --mount source=blc-frontend,target=/blivechat/frontend/dist \
+     xfgryujk/blivechat:latest
    ```
 2. 用浏览器打开[http://localhost:12450](http://localhost:12450),以下略
 

From 7a244757d29bd69c3cb9e9a5b506b6df3947c3af Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 5 Sep 2020 09:44:40 +0800
Subject: [PATCH 16/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8DOBS=2025.0.8=E4=B8=8D?=
 =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=AD=E6=96=87=E5=AD=97=E4=BD=93=E5=90=8D?=
 =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 frontend/src/views/StyleGenerator/stylegen.js | 41 +++++++++++++++----
 1 file changed, 33 insertions(+), 8 deletions(-)

diff --git a/frontend/src/views/StyleGenerator/stylegen.js b/frontend/src/views/StyleGenerator/stylegen.js
index 9af34825..6155afd4 100644
--- a/frontend/src/views/StyleGenerator/stylegen.js
+++ b/frontend/src/views/StyleGenerator/stylegen.js
@@ -108,7 +108,7 @@ yt-live-chat-author-chip #author-name {
 /* Outlines */
 yt-live-chat-renderer * {
   ${getShowOutlinesStyle(config)}
-  font-family: "${config.messageFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.messageFont)}"${FALLBACK_FONTS};
   font-size: ${config.messageFontSize}px !important;
   line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
 }
@@ -162,7 +162,7 @@ yt-live-chat-text-message-renderer #chat-badges {
 yt-live-chat-text-message-renderer #timestamp {
   display: ${config.showTime ? 'inline' : 'none'} !important;
   ${config.timeColor ? `color: ${config.timeColor} !important;` : ''}
-  font-family: "${config.timeFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.timeFont)}"${FALLBACK_FONTS};
   font-size: ${config.timeFontSize}px !important;
   line-height: ${config.timeLineHeight || config.timeFontSize}px !important;
 }
@@ -187,7 +187,7 @@ yt-live-chat-text-message-renderer yt-live-chat-author-badge-renderer[type="memb
 yt-live-chat-text-message-renderer #author-name {
   ${config.showUserNames ? '' : 'display: none !important;'}
   ${config.userNameColor ? `color: ${config.userNameColor} !important;` : ''}
-  font-family: "${config.userNameFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.userNameFont)}"${FALLBACK_FONTS};
   font-size: ${config.userNameFontSize}px !important;
   line-height: ${config.userNameLineHeight || config.userNameFontSize}px !important;
 }
@@ -198,7 +198,7 @@ ${getShowColonStyle(config)}
 yt-live-chat-text-message-renderer #message,
 yt-live-chat-text-message-renderer #message * {
   ${config.messageColor ? `color: ${config.messageColor} !important;` : ''}
-  font-family: "${config.messageFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.messageFont)}"${FALLBACK_FONTS};
   font-size: ${config.messageFontSize}px !important;
   line-height: ${config.messageLineHeight || config.messageFontSize}px !important;
 }
@@ -213,7 +213,7 @@ yt-live-chat-paid-message-renderer #author-name *,
 yt-live-chat-membership-item-renderer #header-content-inner-column,
 yt-live-chat-membership-item-renderer #header-content-inner-column * {
   ${config.firstLineColor ? `color: ${config.firstLineColor} !important;` : ''}
-  font-family: "${config.firstLineFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.firstLineFont)}"${FALLBACK_FONTS};
   font-size: ${config.firstLineFontSize}px !important;
   line-height: ${config.firstLineLineHeight || config.firstLineFontSize}px !important;
 }
@@ -223,7 +223,7 @@ yt-live-chat-paid-message-renderer #purchase-amount *,
 yt-live-chat-membership-item-renderer #header-subtext,
 yt-live-chat-membership-item-renderer #header-subtext * {
   ${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
-  font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.secondLineFont)}"${FALLBACK_FONTS};
   font-size: ${config.secondLineFontSize}px !important;
   line-height: ${config.secondLineLineHeight || config.secondLineFontSize}px !important;
 }
@@ -231,7 +231,7 @@ yt-live-chat-membership-item-renderer #header-subtext * {
 yt-live-chat-paid-message-renderer #content,
 yt-live-chat-paid-message-renderer #content * {
   ${config.scContentColor ? `color: ${config.scContentColor} !important;` : ''}
-  font-family: "${config.scContentFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.scContentFont)}"${FALLBACK_FONTS};
   font-size: ${config.scContentFontSize}px !important;
   line-height: ${config.scContentLineHeight || config.scContentFontSize}px !important;
 }
@@ -273,7 +273,7 @@ yt-live-chat-ticker-paid-message-item-renderer *,
 yt-live-chat-ticker-sponsor-item-renderer,
 yt-live-chat-ticker-sponsor-item-renderer * {
   ${config.secondLineColor ? `color: ${config.secondLineColor} !important;` : ''}
-  font-family: "${config.secondLineFont}"${FALLBACK_FONTS};
+  font-family: "${cssEscapeStr(config.secondLineFont)}"${FALLBACK_FONTS};
 }
 
 yt-live-chat-mode-change-message-renderer, 
@@ -337,6 +337,31 @@ function getShowOutlinesStyle (config) {
   return `text-shadow: ${shadow.join(', ')};`
 }
 
+function cssEscapeStr (str) {
+  let res = []
+  for (let char of str) {
+    res.push(cssEscapeChar(char))
+  }
+  return res.join('')
+}
+
+function cssEscapeChar (char) {
+  if (!needEscapeChar(char)) {
+    return char
+  }
+  let hexCode = char.codePointAt(0).toString(16)
+  // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
+  return `\\${hexCode} `
+}
+
+function needEscapeChar (char) {
+  let code = char.codePointAt(0)
+  if (0x20 <= code && code <= 0x7E) {
+    return char === '"'
+  }
+  return true
+}
+
 function getPaddingStyle (config) {
   return `padding-left: ${config.useBarsInsteadOfBg ? 20 : 4}px !important;
   padding-right: 4px !important;`

From 152db9f00a48599d38ce176be03a049044d0d3ea Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 5 Sep 2020 12:05:01 +0800
Subject: [PATCH 17/18] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?=
 =?UTF-8?q?=E5=B9=B3=E6=BB=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/ChatRenderer/index.vue     | 34 ++++++++++++-------
 1 file changed, 22 insertions(+), 12 deletions(-)

diff --git a/frontend/src/components/ChatRenderer/index.vue b/frontend/src/components/ChatRenderer/index.vue
index dce06cf5..34674336 100644
--- a/frontend/src/components/ChatRenderer/index.vue
+++ b/frontend/src/components/ChatRenderer/index.vue
@@ -281,16 +281,25 @@ export default {
       if (this.estimatedEnqueueInterval) {
         estimatedNextEnqueueRemainTime = Math.max(this.lastEnqueueTime - new Date() + this.estimatedEnqueueInterval, 1)
       }
-      // 最快80ms/条,计算发送的消息数,保证在下次进队列之前消费完队列
+      // 最快80ms/条,计算发送的消息数,保证在下次进队列之前消费队列到最多剩3条消息,不消费完是为了防止消息速度变慢时突然停顿
+      const MIN_SLEEP_TIME = 80
+      const MAX_SLEEP_TIME = 1000
+      const MAX_REMAIN_GROUP_NUM = 3
+      // 下次进队列之前应该发多少条消息
+      let shouldEmitGroupNum = Math.max(this.smoothedMessageQueue.length - MAX_REMAIN_GROUP_NUM, 0)
+      // 下次进队列之前最多能发多少次
+      let maxCanEmitCount = estimatedNextEnqueueRemainTime / MIN_SLEEP_TIME
+      // 这次发多少条消息
       let groupNumToEmit
-      if (this.smoothedMessageQueue.length < estimatedNextEnqueueRemainTime / 80) {
-        // 队列中消息数很少,每次发1条也能发完
+      if (shouldEmitGroupNum < maxCanEmitCount) {
+        // 队列中消息数很少,每次发1条也能发到最多剩3条
         groupNumToEmit = 1
       } else {
-        // 每次发1条以上,保证按最快速度能发完
-        groupNumToEmit = Math.ceil(this.smoothedMessageQueue.length / (estimatedNextEnqueueRemainTime / 80))
+        // 每次发1条以上,保证按最快速度能发到最多剩3条
+        groupNumToEmit = Math.ceil(shouldEmitGroupNum / maxCanEmitCount)
       }
 
+      // 发消息
       let messageGroups = this.smoothedMessageQueue.splice(0, groupNumToEmit)
       let mergedGroup = []
       for (let messageGroup of messageGroups) {
@@ -303,19 +312,20 @@ export default {
       if (this.smoothedMessageQueue.length <= 0) {
         return
       }
+      // 消息没发完,计算下次发消息时间
       let sleepTime
-      if (groupNumToEmit == 1) {
-        // 队列中消息数很少,随便定个80-1000ms的时间
+      if (groupNumToEmit === 1) {
+        // 队列中消息数很少,随便定个[MIN_SLEEP_TIME, MAX_SLEEP_TIME]的时间
         sleepTime = estimatedNextEnqueueRemainTime / this.smoothedMessageQueue.length
         sleepTime *= 0.5 + Math.random()
-        if (sleepTime > 1000) {
-          sleepTime = 1000
-        } else if (sleepTime < 80) {
-          sleepTime = 80
+        if (sleepTime > MAX_SLEEP_TIME) {
+          sleepTime = MAX_SLEEP_TIME
+        } else if (sleepTime < MIN_SLEEP_TIME) {
+          sleepTime = MIN_SLEEP_TIME
         }
       } else {
         // 按最快速度发
-        sleepTime = 80
+        sleepTime = MIN_SLEEP_TIME
       }
       this.emitSmoothedMessageTimerId = window.setTimeout(this.emitSmoothedMessages, sleepTime)
     },

From 47d7528b61f6908ea784e47ef3961c7cf888f03d Mon Sep 17 00:00:00 2001
From: John Smith <xfgryujk@126.com>
Date: Sat, 5 Sep 2020 12:08:17 +0800
Subject: [PATCH 18/18] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?=
 =?UTF-8?q?=E5=8F=B7v1.4.5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 frontend/src/layout/index.vue | 2 +-
 update.py                     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue
index 5f371956..153c22a9 100644
--- a/frontend/src/layout/index.vue
+++ b/frontend/src/layout/index.vue
@@ -9,7 +9,7 @@
         </router-link>
       </div>
       <div class="version">
-        v1.4.4
+        v1.4.5
       </div>
       <sidebar></sidebar>
     </el-aside>
diff --git a/update.py b/update.py
index c3238478..6d66c6f8 100644
--- a/update.py
+++ b/update.py
@@ -4,7 +4,7 @@
 
 import aiohttp
 
-VERSION = 'v1.4.4'
+VERSION = 'v1.4.5'
 
 
 def check_update():