diff --git a/package-lock.json b/package-lock.json
index cedc7f81..92f2498b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
         "@justinribeiro/lite-youtube": "~1.5.0",
         "@szhsin/react-menu": "~4.1.0",
         "@uidotdev/usehooks": "~2.4.1",
+        "compare-versions": "~6.1.0",
         "dayjs": "~1.11.10",
         "dayjs-twitter": "~0.5.0",
         "fast-blurhash": "~1.1.2",
@@ -22,11 +23,11 @@
         "idb-keyval": "~6.2.1",
         "just-debounce-it": "~3.2.0",
         "lz-string": "~1.5.0",
-        "masto": "~6.3.3",
+        "masto": "~6.4.2",
         "moize": "~6.1.6",
         "p-retry": "~6.1.0",
         "p-throttle": "~5.1.0",
-        "preact": "~10.18.1",
+        "preact": "~10.18.2",
         "react-hotkeys-hook": "~4.4.1",
         "react-intersection-observer": "~9.5.2",
         "react-quick-pinch-zoom": "~5.0.0",
@@ -45,12 +46,12 @@
         "@trivago/prettier-plugin-sort-imports": "~4.2.1",
         "postcss": "~8.4.31",
         "postcss-dark-theme-class": "~1.0.0",
-        "postcss-preset-env": "~9.2.0",
+        "postcss-preset-env": "~9.3.0",
         "twitter-text": "~3.1.0",
         "vite": "~4.5.0",
         "vite-plugin-generate-file": "~0.0.4",
         "vite-plugin-html-config": "~1.0.11",
-        "vite-plugin-pwa": "~0.16.5",
+        "vite-plugin-pwa": "~0.16.6",
         "vite-plugin-remove-console": "~2.1.1",
         "workbox-cacheable-response": "~7.0.0",
         "workbox-expiration": "~7.0.0",
@@ -1988,9 +1989,9 @@
       }
     },
     "node_modules/@csstools/postcss-cascade-layers": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.0.tgz",
-      "integrity": "sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.1.tgz",
+      "integrity": "sha512-UYFuFL9GgVnftg9v7tBvVEBRLaBeAD66euD+yYy5fYCUld9ZIWTJNCE30hm6STMEdt6FL5xzeVw1lAZ1tpvUEg==",
       "dev": true,
       "funding": [
         {
@@ -2299,6 +2300,50 @@
         "postcss": "^8.4"
       }
     },
+    "node_modules/@csstools/postcss-logical-overflow": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.0.tgz",
+      "integrity": "sha512-cIrZ8f7bGGvr+W53nEuMspcwaeaI2YTmz6LZ4yiAO5z14/PQgOOv+Pn+qjvPOPoadeY2BmpaoTzZKvdAQuM17w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
+    "node_modules/@csstools/postcss-logical-overscroll-behavior": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.0.tgz",
+      "integrity": "sha512-e89S2LWjnxf0SB2wNUAbqDyFb/Fow/tlOe1XqOLbNx4rf3LrQokM9qldVx7sarnddml3ORE5LDUmlKpPOOeJTA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "engines": {
+        "node": "^14 || ^16 || >=18"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4"
+      }
+    },
     "node_modules/@csstools/postcss-logical-resize": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.0.tgz",
@@ -3870,6 +3915,11 @@
         "node": ">=4.0.0"
       }
     },
+    "node_modules/compare-versions": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
+      "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3997,9 +4047,9 @@
       }
     },
     "node_modules/cssdb": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.8.0.tgz",
-      "integrity": "sha512-SkeezZOQr5AHt9MgJgSFNyiuJwg1p8AwoVln6JwaQJsyxduRW9QJ+HP/gAQzbsz8SIqINtYvpJKjxTRI67zxLg==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
+      "integrity": "sha512-WPMT9seTQq6fPAa1yN4zjgZZeoTriSN2LqW9C+otjar12DQIWA4LuSfFrvFJiKp4oD0xIk1vumDLw8K9ur4NBw==",
       "dev": true,
       "funding": [
         {
@@ -4264,9 +4314,9 @@
       }
     },
     "node_modules/events-to-async": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.0.tgz",
-      "integrity": "sha512-NiZEr4g51nI4/lz/6NdwMqK/TLIctlnp9TQ3wCJjlRp47VgrthUZE4nrk2UhfZ8VzoQ/Xyth+G6MKioLCt0FVA=="
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.1.tgz",
+      "integrity": "sha512-RtnLYrMbXp4JkZIoZu+3VTqV21bNVBlJBZ4NmtwvMNqSE3qouhxv2gvLE4JJDaQc54ioPkrX74V6x+hp/hqjkQ=="
     },
     "node_modules/fast-blurhash": {
       "version": "1.1.2",
@@ -5257,12 +5307,12 @@
       }
     },
     "node_modules/masto": {
-      "version": "6.3.3",
-      "resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
-      "integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/masto/-/masto-6.4.2.tgz",
+      "integrity": "sha512-aIxhsTkl0pc755Sf9NMuglY0VM9HuU/tec60e4oLsyyzAkE7ELwufh6anErO84n8g3eWu/u0LGucTujIiDaAhg==",
       "dependencies": {
         "change-case": "^4.1.2",
-        "events-to-async": "^2.0.0",
+        "events-to-async": "^2.0.1",
         "isomorphic-ws": "^5.0.0",
         "ts-custom-error": "^3.3.1",
         "ws": "^8.13.0"
@@ -6087,9 +6137,9 @@
       }
     },
     "node_modules/postcss-preset-env": {
-      "version": "9.2.0",
-      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.2.0.tgz",
-      "integrity": "sha512-Lnr4C5gb7t5Cc8akQMJzNdJkqw7s7s7BHUaQSgsuf+CTY9Lsz5lqQTft5yNZr59JyCLz0aFNSAqSLm/xRtcTpg==",
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz",
+      "integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==",
       "dev": true,
       "funding": [
         {
@@ -6102,7 +6152,7 @@
         }
       ],
       "dependencies": {
-        "@csstools/postcss-cascade-layers": "^4.0.0",
+        "@csstools/postcss-cascade-layers": "^4.0.1",
         "@csstools/postcss-color-function": "^3.0.7",
         "@csstools/postcss-color-mix-function": "^2.0.7",
         "@csstools/postcss-exponential-functions": "^1.0.1",
@@ -6114,6 +6164,8 @@
         "@csstools/postcss-initial": "^1.0.0",
         "@csstools/postcss-is-pseudo-class": "^4.0.3",
         "@csstools/postcss-logical-float-and-clear": "^2.0.0",
+        "@csstools/postcss-logical-overflow": "^1.0.0",
+        "@csstools/postcss-logical-overscroll-behavior": "^1.0.0",
         "@csstools/postcss-logical-resize": "^2.0.0",
         "@csstools/postcss-logical-viewport-units": "^2.0.3",
         "@csstools/postcss-media-minmax": "^1.1.0",
@@ -6133,7 +6185,7 @@
         "css-blank-pseudo": "^6.0.0",
         "css-has-pseudo": "^6.0.0",
         "css-prefers-color-scheme": "^9.0.0",
-        "cssdb": "^7.8.0",
+        "cssdb": "^7.9.0",
         "postcss-attribute-case-insensitive": "^6.0.2",
         "postcss-clamp": "^4.1.0",
         "postcss-color-functional-notation": "^6.0.2",
@@ -6241,9 +6293,9 @@
       "dev": true
     },
     "node_modules/preact": {
-      "version": "10.18.1",
-      "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz",
-      "integrity": "sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==",
+      "version": "10.18.2",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.2.tgz",
+      "integrity": "sha512-X/K43vocUHDg0XhWVmTTMbec4LT/iBMh+csCEqJk+pJqegaXsvjdqN80ZZ3L+93azWCnWCZ+WGwYb8SplxeNjA==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/preact"
@@ -7394,9 +7446,9 @@
       }
     },
     "node_modules/vite-plugin-pwa": {
-      "version": "0.16.5",
-      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.5.tgz",
-      "integrity": "sha512-Ahol4dwhMP2UHPQXkllSlXbihOaDFnvBIDPmAxoSZ1EObBUJGP4CMRyCyAVkIHjd6/H+//vH0DM2ON+XxHr81g==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
+      "integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
       "dev": true,
       "dependencies": {
         "debug": "^4.3.4",
@@ -7412,7 +7464,7 @@
         "url": "https://github.com/sponsors/antfu"
       },
       "peerDependencies": {
-        "vite": "^3.1.0 || ^4.0.0",
+        "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0",
         "workbox-build": "^7.0.0",
         "workbox-window": "^7.0.0"
       }
@@ -9113,9 +9165,9 @@
       "requires": {}
     },
     "@csstools/postcss-cascade-layers": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.0.tgz",
-      "integrity": "sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.1.tgz",
+      "integrity": "sha512-UYFuFL9GgVnftg9v7tBvVEBRLaBeAD66euD+yYy5fYCUld9ZIWTJNCE30hm6STMEdt6FL5xzeVw1lAZ1tpvUEg==",
       "dev": true,
       "requires": {
         "@csstools/selector-specificity": "^3.0.0",
@@ -9234,6 +9286,20 @@
       "dev": true,
       "requires": {}
     },
+    "@csstools/postcss-logical-overflow": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-1.0.0.tgz",
+      "integrity": "sha512-cIrZ8f7bGGvr+W53nEuMspcwaeaI2YTmz6LZ4yiAO5z14/PQgOOv+Pn+qjvPOPoadeY2BmpaoTzZKvdAQuM17w==",
+      "dev": true,
+      "requires": {}
+    },
+    "@csstools/postcss-logical-overscroll-behavior": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-1.0.0.tgz",
+      "integrity": "sha512-e89S2LWjnxf0SB2wNUAbqDyFb/Fow/tlOe1XqOLbNx4rf3LrQokM9qldVx7sarnddml3ORE5LDUmlKpPOOeJTA==",
+      "dev": true,
+      "requires": {}
+    },
     "@csstools/postcss-logical-resize": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-2.0.0.tgz",
@@ -10185,6 +10251,11 @@
       "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
       "dev": true
     },
+    "compare-versions": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
+      "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -10256,9 +10327,9 @@
       "requires": {}
     },
     "cssdb": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.8.0.tgz",
-      "integrity": "sha512-SkeezZOQr5AHt9MgJgSFNyiuJwg1p8AwoVln6JwaQJsyxduRW9QJ+HP/gAQzbsz8SIqINtYvpJKjxTRI67zxLg==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.9.0.tgz",
+      "integrity": "sha512-WPMT9seTQq6fPAa1yN4zjgZZeoTriSN2LqW9C+otjar12DQIWA4LuSfFrvFJiKp4oD0xIk1vumDLw8K9ur4NBw==",
       "dev": true
     },
     "cssesc": {
@@ -10453,9 +10524,9 @@
       "dev": true
     },
     "events-to-async": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.0.tgz",
-      "integrity": "sha512-NiZEr4g51nI4/lz/6NdwMqK/TLIctlnp9TQ3wCJjlRp47VgrthUZE4nrk2UhfZ8VzoQ/Xyth+G6MKioLCt0FVA=="
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/events-to-async/-/events-to-async-2.0.1.tgz",
+      "integrity": "sha512-RtnLYrMbXp4JkZIoZu+3VTqV21bNVBlJBZ4NmtwvMNqSE3qouhxv2gvLE4JJDaQc54ioPkrX74V6x+hp/hqjkQ=="
     },
     "fast-blurhash": {
       "version": "1.1.2",
@@ -11188,12 +11259,12 @@
       }
     },
     "masto": {
-      "version": "6.3.3",
-      "resolved": "https://registry.npmjs.org/masto/-/masto-6.3.3.tgz",
-      "integrity": "sha512-hmDsiscImeZfpkS+5oEWk3w5mkbxERFKN/UpuaoKZpVWWoGWCNnO7iPfQHygs/phP7PQqS6pVHlE5ylqSylf6A==",
+      "version": "6.4.2",
+      "resolved": "https://registry.npmjs.org/masto/-/masto-6.4.2.tgz",
+      "integrity": "sha512-aIxhsTkl0pc755Sf9NMuglY0VM9HuU/tec60e4oLsyyzAkE7ELwufh6anErO84n8g3eWu/u0LGucTujIiDaAhg==",
       "requires": {
         "change-case": "^4.1.2",
-        "events-to-async": "^2.0.0",
+        "events-to-async": "^2.0.1",
         "isomorphic-ws": "^5.0.0",
         "ts-custom-error": "^3.3.1",
         "ws": "^8.13.0"
@@ -11620,12 +11691,12 @@
       }
     },
     "postcss-preset-env": {
-      "version": "9.2.0",
-      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.2.0.tgz",
-      "integrity": "sha512-Lnr4C5gb7t5Cc8akQMJzNdJkqw7s7s7BHUaQSgsuf+CTY9Lsz5lqQTft5yNZr59JyCLz0aFNSAqSLm/xRtcTpg==",
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-9.3.0.tgz",
+      "integrity": "sha512-ycw6doPrqV6QxDCtgiyGDef61bEfiSc59HGM4gOw/wxQxmKnhuEery61oOC/5ViENz/ycpRsuhTexs1kUBTvVw==",
       "dev": true,
       "requires": {
-        "@csstools/postcss-cascade-layers": "^4.0.0",
+        "@csstools/postcss-cascade-layers": "^4.0.1",
         "@csstools/postcss-color-function": "^3.0.7",
         "@csstools/postcss-color-mix-function": "^2.0.7",
         "@csstools/postcss-exponential-functions": "^1.0.1",
@@ -11637,6 +11708,8 @@
         "@csstools/postcss-initial": "^1.0.0",
         "@csstools/postcss-is-pseudo-class": "^4.0.3",
         "@csstools/postcss-logical-float-and-clear": "^2.0.0",
+        "@csstools/postcss-logical-overflow": "^1.0.0",
+        "@csstools/postcss-logical-overscroll-behavior": "^1.0.0",
         "@csstools/postcss-logical-resize": "^2.0.0",
         "@csstools/postcss-logical-viewport-units": "^2.0.3",
         "@csstools/postcss-media-minmax": "^1.1.0",
@@ -11656,7 +11729,7 @@
         "css-blank-pseudo": "^6.0.0",
         "css-has-pseudo": "^6.0.0",
         "css-prefers-color-scheme": "^9.0.0",
-        "cssdb": "^7.8.0",
+        "cssdb": "^7.9.0",
         "postcss-attribute-case-insensitive": "^6.0.2",
         "postcss-clamp": "^4.1.0",
         "postcss-color-functional-notation": "^6.0.2",
@@ -11727,9 +11800,9 @@
       "dev": true
     },
     "preact": {
-      "version": "10.18.1",
-      "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.1.tgz",
-      "integrity": "sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg=="
+      "version": "10.18.2",
+      "resolved": "https://registry.npmjs.org/preact/-/preact-10.18.2.tgz",
+      "integrity": "sha512-X/K43vocUHDg0XhWVmTTMbec4LT/iBMh+csCEqJk+pJqegaXsvjdqN80ZZ3L+93azWCnWCZ+WGwYb8SplxeNjA=="
     },
     "prettier": {
       "version": "2.8.0",
@@ -12507,9 +12580,9 @@
       "requires": {}
     },
     "vite-plugin-pwa": {
-      "version": "0.16.5",
-      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.5.tgz",
-      "integrity": "sha512-Ahol4dwhMP2UHPQXkllSlXbihOaDFnvBIDPmAxoSZ1EObBUJGP4CMRyCyAVkIHjd6/H+//vH0DM2ON+XxHr81g==",
+      "version": "0.16.6",
+      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.6.tgz",
+      "integrity": "sha512-bQPDOWvhPMwydMoWqohXvIzvrq4X8iuCF+q95qEiaM4yC0ybViGKWMnWcpWp0vcnoLk7QvxHDlK65KUZvqB3Sg==",
       "dev": true,
       "requires": {
         "debug": "^4.3.4",
diff --git a/package.json b/package.json
index 40c6aa0d..4a16dc92 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "@justinribeiro/lite-youtube": "~1.5.0",
     "@szhsin/react-menu": "~4.1.0",
     "@uidotdev/usehooks": "~2.4.1",
+    "compare-versions": "~6.1.0",
     "dayjs": "~1.11.10",
     "dayjs-twitter": "~0.5.0",
     "fast-blurhash": "~1.1.2",
@@ -24,11 +25,11 @@
     "idb-keyval": "~6.2.1",
     "just-debounce-it": "~3.2.0",
     "lz-string": "~1.5.0",
-    "masto": "~6.3.3",
+    "masto": "~6.4.2",
     "moize": "~6.1.6",
     "p-retry": "~6.1.0",
     "p-throttle": "~5.1.0",
-    "preact": "~10.18.1",
+    "preact": "~10.18.2",
     "react-hotkeys-hook": "~4.4.1",
     "react-intersection-observer": "~9.5.2",
     "react-quick-pinch-zoom": "~5.0.0",
@@ -47,12 +48,12 @@
     "@trivago/prettier-plugin-sort-imports": "~4.2.1",
     "postcss": "~8.4.31",
     "postcss-dark-theme-class": "~1.0.0",
-    "postcss-preset-env": "~9.2.0",
+    "postcss-preset-env": "~9.3.0",
     "twitter-text": "~3.1.0",
     "vite": "~4.5.0",
     "vite-plugin-generate-file": "~0.0.4",
     "vite-plugin-html-config": "~1.0.11",
-    "vite-plugin-pwa": "~0.16.5",
+    "vite-plugin-pwa": "~0.16.6",
     "vite-plugin-remove-console": "~2.1.1",
     "workbox-cacheable-response": "~7.0.0",
     "workbox-expiration": "~7.0.0",
diff --git a/src/app.css b/src/app.css
index e0d41fa0..a196c571 100644
--- a/src/app.css
+++ b/src/app.css
@@ -56,6 +56,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   overscroll-behavior: contain;
   scroll-behavior: smooth;
   background-color: var(--bg-color);
+  flex-grow: 1;
   /* This `transform` fixes carousel blocking vertical scrolling for pointer devices on iPad */
   transform: translateZ(0);
 }
@@ -79,6 +80,21 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   scroll-padding-top: 3em;
 }
 
+:is(#home-page, #welcome, #columns, #loader-root) ~ .deck-container {
+  z-index: 10;
+  position: fixed;
+  inset: 0;
+}
+:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
+  display: block;
+  position: absolute;
+  user-select: none;
+  pointer-events: none;
+  opacity: 0;
+  /* This causes scrollTop to be reset to 0 when the page is hidden */
+  /* content-visibility: hidden; */
+}
+
 .deck {
   min-height: 100vh;
   min-height: 100dvh;
@@ -110,6 +126,16 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   user-select: none;
   transition: transform 0.5s ease-in-out;
   user-select: none;
+
+  .header-double-lines {
+    font-size: 90% !important;
+    cursor: pointer;
+
+    div {
+      font-weight: normal;
+      color: var(--text-insignificant-color);
+    }
+  }
 }
 .deck > header[hidden] {
   display: block;
@@ -209,6 +235,64 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
 .timeline {
   margin: 0 auto;
   padding: 0;
+
+  &.timeline-media {
+    --grid-gap: 8px;
+    display: grid;
+    grid-template-columns: 1fr;
+    grid-auto-rows: fit-content;
+    gap: var(--grid-gap);
+    padding: var(--grid-gap);
+
+    &:not(#columns &) {
+      background-color: var(--bg-faded-color);
+    }
+
+    @media (min-width: 320px) {
+      grid-template-columns: 1fr 1fr;
+    }
+
+    @media (min-width: 40em) {
+      &:not(#columns &) {
+        --grid-gap: 16px;
+        grid-template-columns: 1fr 1fr 1fr;
+
+        @media (min-width: 40em) {
+          width: 95vw;
+          max-width: calc(320px * 3.3);
+          transform: translateX(calc(-50% + var(--main-width) / 2));
+        }
+      }
+
+      #columns & {
+        padding-inline: 0;
+      }
+    }
+
+    li {
+      padding: 0 !important;
+      margin: 0 !important;
+      border: 0 !important;
+      overflow: visible !important;
+      background-color: transparent !important;
+      box-shadow: none !important;
+    }
+
+    @supports (grid-template-rows: masonry) {
+      grid-template-rows: masonry;
+      masonry-auto-flow: pack;
+
+      .media-post a {
+        aspect-ratio: revert !important;
+
+        video,
+        img,
+        audio {
+          min-height: 88px; /* for extreme dimensions */
+        }
+      }
+    }
+  }
 }
 .timeline.grow {
   /* min-height: 100vh;
@@ -275,10 +359,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   box-shadow: inset 0 -3px var(--comment-line-color),
     inset 0 3px var(--comment-line-color);
   overscroll-behavior-x: contain;
-
-  .status-link {
-    width: fit-content;
-  }
+  touch-action: pan-x;
 }
 .timeline.contextual .replies[data-comments-level='4'] {
   overflow-x: auto;
@@ -471,7 +552,6 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   border-radius: 8px;
   cursor: pointer;
   text-transform: uppercase;
-  font-weight: 500;
   font-size: 12px;
   color: var(--text-insignificant-color);
   user-select: none;
@@ -479,6 +559,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
   position: relative;
   list-style: none;
   white-space: nowrap;
+
+  b {
+    font-weight: 500;
+    color: var(--text-color);
+  }
 }
 .timeline.contextual > li .replies > .replies-summary::-webkit-details-marker {
   display: none;
@@ -488,6 +573,10 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
 }
 .timeline.contextual > li .replies > .replies-summary .avatars {
   margin-right: 8px;
+
+  > *:not(:first-child) {
+    margin: 0 0 0 -4px;
+  }
 }
 .timeline.contextual > li .replies > .replies-summary:active,
 .timeline.contextual > li .replies[open] > .replies-summary {
@@ -928,10 +1017,13 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
 .deck-backdrop .deck .status {
   max-width: var(--main-width);
 }
-.deck-backdrop .deck .menu-switch-view {
+.deck-backdrop .deck :is(.button-switch-view, .menu-switch-view) {
   display: none;
 }
 @media (min-width: 40em) {
+  .deck-backdrop .deck .button-switch-view {
+    display: inline-block;
+  }
   .deck-backdrop .deck .menu-switch-view {
     display: flex;
   }
@@ -1218,6 +1310,9 @@ button.carousel-dot:is(.active, [disabled].active) .icon {
 body:has(.status-deck) .media-post-link {
   display: none;
 }
+.media-modal-count-1 .button-label {
+  display: inline;
+}
 
 /* ✨ New */
 body:has(.media-modal-container + .status-deck) .media-post-link {
@@ -1675,6 +1770,24 @@ body > .szh-menu-container {
   opacity: 1;
 }
 
+.szh-menu
+  .szh-menu__item--type-checkbox:not(.szh-menu__item--disabled):not(
+    .szh-menu__item--hover
+  ) {
+  .icon {
+    opacity: 0.15;
+  }
+
+  &.szh-menu__item--checked {
+    color: var(--link-color);
+
+    .icon {
+      opacity: 1;
+      color: inherit;
+    }
+  }
+}
+
 .szh-menu .menu-wrap {
   display: flex;
   flex-wrap: wrap;
@@ -1843,25 +1956,6 @@ meter.donut[hidden] {
   }
 }
 
-.deck-container {
-  width: 100%;
-  flex-grow: 1;
-}
-:is(#home-page, #welcome, #columns, #loader-root) ~ .deck-container {
-  z-index: 10;
-  position: fixed;
-  inset: 0;
-}
-:is(#home-page, #welcome, #columns, #loader-root):has(~ .deck-container) {
-  display: block;
-  position: absolute;
-  user-select: none;
-  pointer-events: none;
-  opacity: 0;
-  /* This causes scrollTop to be reset to 0 when the page is hidden */
-  /* content-visibility: hidden; */
-}
-
 /* 404 */
 
 #not-found-page {
@@ -1882,13 +1976,23 @@ meter.donut[hidden] {
 
 /* ACCOUNT STATUSES */
 
-.header-account {
-  font-size: 90% !important;
-  cursor: pointer;
+@keyframes peekaboo-header {
+  from {
+    opacity: 0;
+    transform: translateY(10%);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
 }
-.header-account div {
-  font-weight: normal;
-  color: var(--text-insignificant-color);
+
+@supports (animation-timeline: scroll()) {
+  .header-account {
+    animation: peekaboo-header 1s linear both;
+    animation-timeline: scroll();
+    animation-range: 0 150px;
+  }
 }
 
 /* LINK LISTS? */
@@ -1917,6 +2021,21 @@ ul.link-list li a {
   display: flex;
   align-items: center;
   gap: 8px;
+
+  .count {
+    font-size: 80%;
+    display: inline-block;
+    color: var(--text-insignificant-color);
+    min-width: 16px;
+    min-height: 16px;
+    padding: 4px;
+    background-color: var(--bg-color);
+    border-radius: 4px;
+
+    @media (min-width: 40em) {
+      background-color: var(--bg-faded-color);
+    }
+  }
 }
 ul.link-list li:first-child a {
   border-top-left-radius: var(--radius);
@@ -2009,13 +2128,18 @@ ul.link-list li a .icon {
 #columns .header-grid input {
   pointer-events: none;
 }
-#columns
-  .header-grid
-  .header-side:first-of-type
-  :is(button, .button)
-  ~ :is(button, .button),
-#columns .deck-container:not(:first-of-type) .header-grid .header-side > * {
-  display: none;
+#columns {
+  /* Any buttons except nav menu button on first header-side, on 1st column */
+  .deck-container:first-of-type
+    .header-grid
+    .header-side:first-of-type
+    > *:not(.nav-menu-button),
+  /* Any buttons on last header-side, on 1st column */
+  .deck-container:first-of-type .header-grid .header-side:last-of-type > *,
+  /* Any buttons on any header-side, on columns after 1st */
+  .deck-container:not(:first-of-type) .header-grid .header-side > * {
+    display: none;
+  }
 }
 @media (min-width: 40em) {
   #columns {
diff --git a/src/app.jsx b/src/app.jsx
index a673a34e..44d937ad 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -10,7 +10,7 @@ import {
 } from 'preact/hooks';
 import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
 import 'swiped-events';
-import { subscribe, useSnapshot } from 'valtio';
+import { subscribe } from 'valtio';
 
 import BackgroundService from './components/background-service';
 import ComposeButton from './components/compose-button';
@@ -49,12 +49,70 @@ import {
 } from './utils/api';
 import { getAccessToken } from './utils/auth';
 import focusDeck from './utils/focus-deck';
-import states, { initStates } from './utils/states';
+import states, { initStates, statusKey } from './utils/states';
 import store from './utils/store';
 import { getCurrentAccount } from './utils/store-utils';
 import './utils/toast-alert';
 
 window.__STATES__ = states;
+window.__STATES_STATS__ = () => {
+  const keys = [
+    'statuses',
+    'accounts',
+    'spoilers',
+    'unfurledLinks',
+    'statusQuotes',
+  ];
+  const counts = {};
+  keys.forEach((key) => {
+    counts[key] = Object.keys(states[key]).length;
+  });
+  console.warn('STATE stats', counts);
+
+  const { statuses } = states;
+  const unmountedPosts = [];
+  for (const key in statuses) {
+    const $post = document.querySelector(`[data-state-post-id="${key}"]`);
+    if (!$post) {
+      unmountedPosts.push(key);
+    }
+  }
+  console.warn('Unmounted posts', unmountedPosts.length, unmountedPosts);
+};
+
+// Experimental "garbage collection" for states
+// Every 15 minutes
+// Only posts for now
+setInterval(() => {
+  if (!window.__IDLE__) return;
+  const { statuses, unfurledLinks, notifications } = states;
+  let keysCount = 0;
+  const { instance } = api();
+  for (const key in statuses) {
+    try {
+      const $post = document.querySelector(`[data-state-post-id~="${key}"]`);
+      const postInNotifications = notifications.some(
+        (n) => key === statusKey(n.status?.id, instance),
+      );
+      if (!$post && !postInNotifications) {
+        delete states.statuses[key];
+        delete states.statusQuotes[key];
+        for (const link in unfurledLinks) {
+          const unfurled = unfurledLinks[link];
+          const sKey = statusKey(unfurled.id, unfurled.instance);
+          if (sKey === key) {
+            delete states.unfurledLinks[link];
+            break;
+          }
+        }
+        keysCount++;
+      }
+    } catch (e) {}
+  }
+  if (keysCount) {
+    console.info(`GC: Removed ${keysCount} keys`);
+  }
+}, 15 * 60 * 1000);
 
 // Preload icons
 // There's probably a better way to do this
@@ -70,7 +128,7 @@ setTimeout(() => {
 }, 5000);
 
 (() => {
-  window.__IDLE__ = false;
+  window.__IDLE__ = true;
   const nonIdleEvents = [
     'mousemove',
     'mousedown',
@@ -81,13 +139,14 @@ setTimeout(() => {
     'pointermove',
     'wheel',
   ];
-  const IDLE_TIME = 5_000; // 5 seconds
-  const setIdle = debounce(() => {
+  const setIdle = () => {
     window.__IDLE__ = true;
-  }, IDLE_TIME);
+  };
+  const IDLE_TIME = 3_000; // 3 seconds
+  const debouncedSetIdle = debounce(setIdle, IDLE_TIME);
   const onNonIdle = () => {
     window.__IDLE__ = false;
-    setIdle();
+    debouncedSetIdle();
   };
   nonIdleEvents.forEach((event) => {
     window.addEventListener(event, onNonIdle, {
@@ -95,6 +154,21 @@ setTimeout(() => {
       capture: true,
     });
   });
+  window.addEventListener('blur', setIdle, {
+    passive: true,
+  });
+  // When cursor leaves the window, set idle
+  document.documentElement.addEventListener(
+    'mouseleave',
+    (e) => {
+      if (!e.relatedTarget && !e.toElement) {
+        setIdle();
+      }
+    },
+    {
+      passive: true,
+    },
+  );
   // document.addEventListener(
   //   'visibilitychange',
   //   () => {
diff --git a/src/cloak-mode.css b/src/cloak-mode.css
index d703b7a9..1c6d780b 100644
--- a/src/cloak-mode.css
+++ b/src/cloak-mode.css
@@ -11,7 +11,7 @@ body.cloak,
   .status .content-compact,
   .account-container :is(header, main > *:not(.actions)),
   .account-container :is(header, main > *:not(.actions)) *,
-  .header-account,
+  .header-double-lines,
   .account-block {
     text-decoration-thickness: 1.1em;
     text-decoration-line: line-through;
@@ -25,6 +25,7 @@ body.cloak,
   }
 
   .status :is(img, video, audio),
+  .media-post .media,
   .avatar,
   .emoji,
   .header-banner {
diff --git a/src/components/account-block.jsx b/src/components/account-block.jsx
index 0b7e9cea..13c680be 100644
--- a/src/components/account-block.jsx
+++ b/src/components/account-block.jsx
@@ -82,7 +82,7 @@ function AccountBlock({
       }}
     >
       <Avatar url={avatar} size={avatarSize} squircle={bot} />
-      <span>
+      <span class="account-block-content">
         {!hideDisplayName && (
           <>
             {displayName ? (
diff --git a/src/components/account-info.css b/src/components/account-info.css
index cfc49e91..e5c00324 100644
--- a/src/components/account-info.css
+++ b/src/components/account-info.css
@@ -18,7 +18,7 @@
   position: absolute;
   top: 8px;
   inset-inline: 8px;
-  z-index: 2;
+  z-index: 3;
   border: 1px solid var(--outline-color);
   box-shadow: 0 8px 16px var(--drop-shadow-color);
   border-radius: calc(16px - 8px);
@@ -47,7 +47,7 @@
 
   ~ * {
     /* pointer-events: none; */
-    filter: grayscale(0.75) brightness(0.75);
+    filter: grayscale(0.75) opacity(0.75);
   }
 }
 
@@ -254,6 +254,7 @@
 .account-container .note {
   font-size: 95%;
   line-height: 1.4;
+  text-wrap: pretty;
 }
 .account-container .note:not(:has(p)):not(:empty) {
   /* Some notes don't have <p> tags, so we need to add some padding */
@@ -408,6 +409,7 @@
 
 .timeline-start .account-container {
   border-bottom: 1px solid var(--outline-color);
+  position: relative;
 }
 .timeline-start .account-container header {
   padding: 16px 16px 1px;
@@ -424,18 +426,46 @@
   display: none;
 }
 
+@keyframes bye-banner {
+  20% {
+    filter: blur(0) opacity(1);
+  }
+  100% {
+    filter: blur(16px) opacity(0.2);
+  }
+}
+@keyframes surface-header {
+  0% {
+    border-bottom-color: transparent;
+    box-shadow: none;
+  }
+  100% {
+    border-bottom-color: var(--outline-color);
+    box-shadow: 0 8px 16px -8px var(--drop-shadow-color);
+  }
+}
+@keyframes shrink-avatar {
+  0% {
+    width: 64px;
+    height: 64px;
+  }
+  100% {
+    width: 2.5em;
+    height: 2.5em;
+  }
+}
 .sheet .account-container {
   border-radius: 16px 16px 0 0;
   overflow-x: hidden;
   max-height: 75vh;
   overscroll-behavior: none;
+  scroll-timeline: --account-scroll;
 
   header {
     padding-bottom: 16px;
     position: sticky;
     top: 0;
     z-index: 2;
-    /* --bg-color: red; */
     background-image: linear-gradient(
       to bottom,
       transparent 30%,
@@ -443,6 +473,14 @@
       var(--bg-color) calc(100% - 8px),
       transparent
     );
+
+    .account-block-content {
+      display: -webkit-box;
+      -webkit-box-orient: vertical;
+      overflow: hidden;
+      line-clamp: 3;
+      -webkit-line-clamp: 3;
+    }
   }
 
   .faux-header-bg {
@@ -455,6 +493,32 @@
     margin-top: calc(-1 * var(--banner-overlap));
   }
 
+  @supports (animation-timeline: scroll()) {
+    .header-banner:not(.header-is-avatar):not(:hover):not(:active) {
+      animation: bye-banner 1s linear both;
+      animation-timeline: view();
+      animation-range: contain 100% cover 100%;
+    }
+
+    header {
+      background-image: linear-gradient(
+        to bottom,
+        transparent 30%,
+        var(--bg-color) var(--banner-overlap)
+      );
+      border-bottom: 1px solid transparent;
+      animation: surface-header 1s linear both;
+      animation-timeline: --account-scroll;
+      animation-range: 0 150px;
+    }
+
+    header .avatar {
+      animation: shrink-avatar 1s linear both;
+      animation-timeline: --account-scroll;
+      animation-range: 0 150px;
+    }
+  }
+
   main {
     margin-top: -8px;
     padding-top: 1px;
@@ -612,43 +676,6 @@
   }
 }
 
-@keyframes shine {
-  0% {
-    left: -100%;
-  }
-  100% {
-    left: 100%;
-  }
-}
-.timeline-start .account-container {
-  position: relative;
-  overflow: hidden;
-}
-.timeline-start .account-container:before {
-  content: '';
-  position: absolute;
-  z-index: 2;
-  width: 100%;
-  height: 100%;
-  background-image: linear-gradient(
-    100deg,
-    rgba(255, 255, 255, 0) 30%,
-    rgba(255, 255, 255, 0.25),
-    rgba(255, 255, 255, 0) 70%
-  );
-  top: 0;
-  left: -100%;
-  pointer-events: none;
-}
-@media (prefers-color-scheme: dark) {
-  .timeline-start .account-container:before {
-    opacity: 0.25;
-  }
-}
-.timeline-start .account-container:hover:before {
-  animation: shine 1s ease-in-out 1s;
-}
-
 #list-add-remove-container .list-add-remove {
   display: flex;
   flex-direction: column;
diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx
index 3e56205b..f3b2ecaa 100644
--- a/src/components/account-info.jsx
+++ b/src/components/account-info.jsx
@@ -126,19 +126,14 @@ function AccountInfo({
   const { masto } = api({
     instance,
   });
-  const { masto: currentMasto } = api();
+  const { masto: currentMasto, instance: currentInstance } = api();
   const [uiState, setUIState] = useState('default');
   const isString = typeof account === 'string';
   const [info, setInfo] = useState(isString ? null : account);
 
-  const isSelf = useMemo(
-    () => account.id === store.session.get('currentAccount'),
-    [account?.id],
-  );
-
   const sameCurrentInstance = useMemo(
-    () => instance === api().instance,
-    [instance],
+    () => instance === currentInstance,
+    [instance, currentInstance],
   );
 
   useEffect(() => {
@@ -198,6 +193,37 @@ function AccountInfo({
     }
   }
 
+  const isSelf = useMemo(
+    () => id === store.session.get('currentAccount'),
+    [id],
+  );
+
+  useEffect(() => {
+    const infoHasEssentials = !!(
+      info?.id &&
+      info?.username &&
+      info?.acct &&
+      info?.avatar &&
+      info?.avatarStatic &&
+      info?.displayName &&
+      info?.url
+    );
+    if (isSelf && instance && infoHasEssentials) {
+      const accounts = store.local.getJSON('accounts');
+      let updated = false;
+      accounts.forEach((account) => {
+        if (account.info.id === info.id && account.instanceURL === instance) {
+          account.info = info;
+          updated = true;
+        }
+      });
+      if (updated) {
+        console.log('Updated account info', info);
+        store.local.setJSON('accounts', accounts);
+      }
+    }
+  }, [isSelf, info, instance]);
+
   const accountInstance = useMemo(() => {
     if (!url) return null;
     const domain = new URL(url).hostname;
@@ -304,12 +330,13 @@ function AccountInfo({
     ({ relationship, currentID }) => {
       if (!relationship.following) {
         renderFamiliarFollowers(currentID);
-        if (!standalone) {
+        if (!standalone && statusesCount > 0) {
+          // Only render posting stats if not standalone and has posts
           renderPostingStats();
         }
       }
     },
-    [standalone, id],
+    [standalone, id, statusesCount],
   );
 
   return (
@@ -534,7 +561,7 @@ function AccountInfo({
                 class="note"
                 dir="auto"
                 onClick={handleContentLinks({
-                  instance,
+                  instance: currentInstance,
                 })}
                 dangerouslySetInnerHTML={{
                   __html: enhanceContent(note, { emojis }),
diff --git a/src/components/avatar.css b/src/components/avatar.css
index 743793b0..41325817 100644
--- a/src/components/avatar.css
+++ b/src/components/avatar.css
@@ -8,24 +8,31 @@
   box-shadow: 0 0 0 1px var(--bg-blur-color);
   flex-shrink: 0;
   vertical-align: middle;
-}
-.avatar.has-alpha {
-  border-radius: 0;
-}
-.avatar:not(.has-alpha).squircle {
-  border-radius: 25%;
-}
 
-.avatar img {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  background-color: var(--img-bg-color);
-  contain: none;
-}
+  &.has-alpha {
+    border-radius: 0;
+    background-color: transparent;
+    box-shadow: none;
 
-.avatar[data-loaded],
-.avatar[data-loaded] img {
-  box-shadow: none;
-  background-color: transparent;
+    img {
+      background-color: transparent;
+    }
+  }
+  &:not(.has-alpha).squircle {
+    border-radius: 25%;
+  }
+
+  img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    background-color: var(--img-bg-color);
+    contain: none;
+  }
+
+  &[data-loaded],
+  &[data-loaded] img {
+    box-shadow: none;
+    background-color: transparent;
+  }
 }
diff --git a/src/components/background-service.jsx b/src/components/background-service.jsx
index e7d7dcfc..6c4b63d1 100644
--- a/src/components/background-service.jsx
+++ b/src/components/background-service.jsx
@@ -1,29 +1,32 @@
 import { memo } from 'preact/compat';
 import { useEffect, useRef, useState } from 'preact/hooks';
-import { useDebouncedCallback } from 'use-debounce';
+import { useHotkeys } from 'react-hotkeys-hook';
 
 import { api } from '../utils/api';
+import showToast from '../utils/show-toast';
 import states, { saveStatus } from '../utils/states';
 import useInterval from '../utils/useInterval';
 import usePageVisibility from '../utils/usePageVisibility';
 
+const STREAMING_TIMEOUT = 1000 * 3; // 3 seconds
+const POLL_INTERVAL = 15_000; // 15 seconds
+
 export default memo(function BackgroundService({ isLoggedIn }) {
   // Notifications service
   // - WebSocket to receive notifications when page is visible
   const [visible, setVisible] = useState(true);
   usePageVisibility(setVisible);
-  const subRef = useRef();
-  const debouncedStartNotifications = useDebouncedCallback(() => {
-    const { masto, streaming, instance } = api();
-    (async () => {
-      // 1. Get the latest notification
-      if (states.notificationsLast) {
-        const notificationsIterator = masto.v1.notifications.list({
-          limit: 1,
-          since_id: states.notificationsLast.id,
-        });
-        const { value: notifications } = await notificationsIterator.next();
-        if (notifications?.length) {
+  const checkLatestNotification = async (masto, instance, skipCheckMarkers) => {
+    if (states.notificationsLast) {
+      const notificationsIterator = masto.v1.notifications.list({
+        limit: 1,
+        sinceId: states.notificationsLast.id,
+      });
+      const { value: notifications } = await notificationsIterator.next();
+      if (notifications?.length) {
+        if (skipCheckMarkers) {
+          states.notificationsShowNew = true;
+        } else {
           let lastReadId;
           try {
             const markers = await masto.v1.markers.fetch({
@@ -38,36 +41,60 @@ export default memo(function BackgroundService({ isLoggedIn }) {
           }
         }
       }
+    }
+  };
 
-      // 2. Start streaming
-      if (streaming) {
-        let sub = (subRef.current = streaming.user.notification.subscribe());
-        console.log('🎏 Streaming notification', sub);
-        for await (const entry of sub) {
-          if (!sub) break;
-          console.log('🔔🔔 Notification entry', entry);
-          if (entry.event === 'notification') {
-            console.log('🔔🔔 Notification', entry);
-            saveStatus(entry.payload, instance, {
-              skipThreading: true,
-            });
-          }
-          states.notificationsShowNew = true;
-        }
-      }
-    })();
-  }, 3000);
   useEffect(() => {
-    // let sub;
+    let sub;
+    let pollNotifications;
     if (isLoggedIn && visible) {
-      debouncedStartNotifications();
+      const { masto, streaming, instance } = api();
+      (async () => {
+        // 1. Get the latest notification
+        await checkLatestNotification(masto, instance);
+
+        let hasStreaming = false;
+        // 2. Start streaming
+        if (streaming) {
+          pollNotifications = setTimeout(() => {
+            (async () => {
+              try {
+                hasStreaming = true;
+                sub = streaming.user.notification.subscribe();
+                console.log('🎏 Streaming notification', sub);
+                for await (const entry of sub) {
+                  if (!sub) break;
+                  if (!visible) break;
+                  console.log('🔔🔔 Notification entry', entry);
+                  if (entry.event === 'notification') {
+                    console.log('🔔🔔 Notification', entry);
+                    saveStatus(entry.payload, instance, {
+                      skipThreading: true,
+                    });
+                  }
+                  states.notificationsShowNew = true;
+                }
+              } catch (e) {
+                hasStreaming = false;
+                console.error(e);
+              }
+
+              if (!hasStreaming) {
+                console.log('🎏 Streaming failed, fallback to polling');
+                pollNotifications = setInterval(() => {
+                  checkLatestNotification(masto, instance, true);
+                }, POLL_INTERVAL);
+              }
+            })();
+          }, STREAMING_TIMEOUT);
+        }
+      })();
     }
     return () => {
-      // sub?.unsubscribe?.();
-      // sub = null;
-      debouncedStartNotifications?.cancel?.();
-      subRef.current?.unsubscribe?.();
-      subRef.current = null;
+      sub?.unsubscribe?.();
+      sub = null;
+      clearTimeout(pollNotifications);
+      clearInterval(pollNotifications);
     };
   }, [visible, isLoggedIn]);
 
@@ -100,5 +127,14 @@ export default memo(function BackgroundService({ isLoggedIn }) {
     }
   });
 
+  // Global keyboard shortcuts "service"
+  useHotkeys('shift+alt+k', () => {
+    const currentCloakMode = states.settings.cloakMode;
+    states.settings.cloakMode = !currentCloakMode;
+    showToast({
+      text: `Cloak mode ${currentCloakMode ? 'disabled' : 'enabled'}`,
+    });
+  });
+
   return null;
 });
diff --git a/src/components/columns.jsx b/src/components/columns.jsx
index 0f1f0237..1c8c64a3 100644
--- a/src/components/columns.jsx
+++ b/src/components/columns.jsx
@@ -49,7 +49,24 @@ function Columns() {
     }
   });
 
-  return <div id="columns">{components}</div>;
+  return (
+    <div
+      id="columns"
+      onContextMenu={(e) => {
+        // If right-click on header, but not links or buttons
+        if (
+          e.target.closest('.deck > header') &&
+          !e.target.closest('a') &&
+          !e.target.closest('button')
+        ) {
+          e.preventDefault();
+          states.showShortcutsSettings = true;
+        }
+      }}
+    >
+      {components}
+    </div>
+  );
 }
 
 export default Columns;
diff --git a/src/components/compose-button.jsx b/src/components/compose-button.jsx
index ceb452c4..3587c2f6 100644
--- a/src/components/compose-button.jsx
+++ b/src/components/compose-button.jsx
@@ -11,7 +11,6 @@ export default function ComposeButton() {
       const newWin = openCompose();
 
       if (!newWin) {
-        alert('Looks like your browser is blocking popups.');
         states.showCompose = true;
       }
     } else {
diff --git a/src/components/compose.css b/src/components/compose.css
index 5531f85e..4870063c 100644
--- a/src/components/compose.css
+++ b/src/components/compose.css
@@ -40,28 +40,6 @@
   border-radius: 9999px;
 }
 
-#compose-container textarea {
-  width: 100%;
-  max-width: 100%;
-  height: 5em;
-  min-height: 5em;
-  max-height: 50vh;
-  resize: vertical;
-  line-height: 1.4;
-  border-color: transparent;
-}
-#compose-container textarea:hover {
-  border-color: var(--divider-color);
-}
-
-@media (min-width: 40em) {
-  #compose-container textarea {
-    /* font-size: 150%;
-    font-size: calc(100% + 50% / var(--text-weight)); */
-    max-height: 65vh;
-  }
-}
-
 @keyframes appear-up {
   0% {
     opacity: 0;
@@ -129,19 +107,53 @@
 }
 
 #compose-container form {
-  border-radius: 16px;
-  padding: 4px 12px;
+  --form-padding-inline: 12px;
+  --form-padding-block: 8px;
+  /* border-radius: 16px; */
+  padding: var(--form-padding-block) var(--form-padding-inline);
   background-color: var(--bg-blur-color);
   /* background-image: linear-gradient(var(--bg-color) 85%, transparent); */
   position: relative;
   z-index: 2;
   --drop-shadow: 0 3px 6px -3px var(--drop-shadow-color);
   box-shadow: var(--drop-shadow);
+
+  @media (min-width: 40em) {
+    border-radius: 16px;
+  }
 }
 #compose-container .status-preview ~ form {
   box-shadow: var(--drop-shadow), 0 -3px 6px -3px var(--drop-shadow-color);
 }
 
+#compose-container textarea {
+  width: 100%;
+  max-width: 100%;
+  height: 5em;
+  min-height: 5em;
+  max-height: 50vh;
+  resize: vertical;
+  line-height: 1.4;
+  border-color: transparent;
+
+  &.compose-field {
+    @media (width < 30em) {
+      margin-inline: calc(-1 * var(--form-padding-inline));
+      width: 100vw !important;
+      max-width: 100vw;
+      border-radius: 0;
+      border: 0;
+    }
+
+    @media (min-width: 40em) {
+      max-height: 65vh;
+    }
+  }
+}
+#compose-container textarea:hover {
+  border-color: var(--divider-color);
+}
+
 #compose-container .toolbar {
   display: flex;
   justify-content: space-between;
@@ -269,6 +281,15 @@
   gap: 8px;
   align-items: center;
   font-size: 90%;
+
+  .grow {
+    flex-grow: 1;
+  }
+
+  .count {
+    font-size: 80%;
+    opacity: 0.5;
+  }
 }
 #compose-container .text-expander-menu li b img {
   /* The shortcode emojis */
diff --git a/src/components/compose.jsx b/src/components/compose.jsx
index b3c213d5..18480db7 100644
--- a/src/components/compose.jsx
+++ b/src/components/compose.jsx
@@ -17,6 +17,7 @@ import db from '../utils/db';
 import emojifyText from '../utils/emojify-text';
 import localeMatch from '../utils/locale-match';
 import openCompose from '../utils/open-compose';
+import shortenNumber from '../utils/shorten-number';
 import states, { saveStatus } from '../utils/states';
 import store from '../utils/store';
 import {
@@ -521,6 +522,34 @@ function Compose({
 
   const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
 
+  const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
+    const topLanguages = [];
+    const restLanguages = [];
+    const { contentTranslationHideLanguages = [] } = states.settings;
+    supportedLanguages.forEach((l) => {
+      const [code] = l;
+      if (
+        code === language ||
+        code === prevLanguage.current ||
+        code === DEFAULT_LANG ||
+        contentTranslationHideLanguages.includes(code)
+      ) {
+        topLanguages.push(l);
+      } else {
+        restLanguages.push(l);
+      }
+    });
+    topLanguages.sort(([codeA, commonA], [codeB, commonB]) => {
+      if (codeA === language) return -1;
+      if (codeB === language) return 1;
+      return commonA.localeCompare(commonB);
+    });
+    restLanguages.sort(([codeA, commonA], [codeB, commonB]) =>
+      commonA.localeCompare(commonB),
+    );
+    return [topLanguages, restLanguages];
+  }, [language]);
+
   return (
     <div id="compose-container-outer">
       <div id="compose-container" class={standalone ? 'standalone' : ''}>
@@ -578,7 +607,6 @@ function Compose({
                   });
 
                   if (!newWin) {
-                    alert('Looks like your browser is blocking popups.');
                     return;
                   }
 
@@ -1125,32 +1153,17 @@ function Compose({
                 }}
                 disabled={uiState === 'loading'}
               >
-                {supportedLanguages
-                  .sort(([codeA, commonA], [codeB, commonB]) => {
-                    const { contentTranslationHideLanguages = [] } =
-                      states.settings;
-                    // Sort codes that same as language, prevLanguage, DEFAULT_LANGUAGE and all the ones in states.settings.contentTranslationHideLanguages, to the top
-                    if (
-                      codeA === language ||
-                      codeA === prevLanguage ||
-                      codeA === DEFAULT_LANG ||
-                      contentTranslationHideLanguages?.includes(codeA)
-                    )
-                      return -1;
-                    if (
-                      codeB === language ||
-                      codeB === prevLanguage ||
-                      codeB === DEFAULT_LANG ||
-                      contentTranslationHideLanguages?.includes(codeB)
-                    )
-                      return 1;
-                    return commonA.localeCompare(commonB);
-                  })
-                  .map(([code, common, native]) => (
-                    <option value={code}>
-                      {common} ({native})
-                    </option>
-                  ))}
+                {topSupportedLanguages.map(([code, common, native]) => (
+                  <option value={code} key={code}>
+                    {common} ({native})
+                  </option>
+                ))}
+                <hr />
+                {restSupportedLanguages.map(([code, common, native]) => (
+                  <option value={code} key={code}>
+                    {common} ({native})
+                  </option>
+                ))}
               </select>
             </label>{' '}
             <button
@@ -1306,6 +1319,7 @@ const Textarea = forwardRef((props, ref) => {
                   username,
                   acct,
                   emojis,
+                  history,
                 } = result;
                 const displayNameWithEmoji = emojifyText(displayName, emojis);
                 // const item = menuItem.cloneNode();
@@ -1324,9 +1338,18 @@ const Textarea = forwardRef((props, ref) => {
                     </li>
                   `;
                 } else {
+                  const total = history?.reduce?.(
+                    (acc, cur) => acc + +cur.uses,
+                    0,
+                  );
                   html += `
                     <li role="option" data-value="${encodeHTML(name)}">
-                      <span>#<b>${encodeHTML(name)}</b></span>
+                      <span class="grow">#<b>${encodeHTML(name)}</b></span>
+                      ${
+                        total
+                          ? `<span class="count">${shortenNumber(total)}</span>`
+                          : ''
+                      }
                     </li>
                   `;
                 }
@@ -1393,6 +1416,7 @@ const Textarea = forwardRef((props, ref) => {
   return (
     <text-expander ref={textExpanderRef} keys="@ # :">
       <textarea
+        class="compose-field"
         autoCapitalize="sentences"
         autoComplete="on"
         autoCorrect="on"
diff --git a/src/components/generic-accounts.jsx b/src/components/generic-accounts.jsx
index 11070c0c..0f38e472 100644
--- a/src/components/generic-accounts.jsx
+++ b/src/components/generic-accounts.jsx
@@ -1,6 +1,6 @@
 import './generic-accounts.css';
 
-import { useEffect, useState } from 'preact/hooks';
+import { useEffect, useRef, useState } from 'preact/hooks';
 import { InView } from 'react-intersection-observer';
 import { useSnapshot } from 'valtio';
 
@@ -56,15 +56,18 @@ export default function GenericAccounts({ onClose = () => {} }) {
     })();
   };
 
+  const firstLoad = useRef(true);
   useEffect(() => {
     if (staticAccounts?.length > 0) {
       setAccounts(staticAccounts);
     } else {
       loadAccounts(true);
+      firstLoad.current = false;
     }
   }, [staticAccounts, fetchAccounts]);
 
   useEffect(() => {
+    if (firstLoad.current) return;
     // reloadGenericAccounts contains value like {id: 'mute', counter: 1}
     // We only need to reload if the id matches
     if (snapStates.reloadGenericAccounts?.id === id) {
diff --git a/src/components/icon.jsx b/src/components/icon.jsx
index 42e33f2a..cd2b5357 100644
--- a/src/components/icon.jsx
+++ b/src/components/icon.jsx
@@ -102,6 +102,7 @@ export const ICONS = {
   keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
   cloud: () => import('@iconify-icons/mingcute/cloud-line'),
   month: () => import('@iconify-icons/mingcute/calendar-month-line'),
+  media: () => import('@iconify-icons/mingcute/photo-album-line'),
 };
 
 function Icon({
diff --git a/src/components/keyboard-shortcuts-help.jsx b/src/components/keyboard-shortcuts-help.jsx
index 08683cb4..df4e437b 100644
--- a/src/components/keyboard-shortcuts-help.jsx
+++ b/src/components/keyboard-shortcuts-help.jsx
@@ -117,6 +117,15 @@ export default memo(function KeyboardShortcutsHelp() {
                   action: 'Compose new post',
                   keys: <kbd>c</kbd>,
                 },
+                {
+                  action: 'Compose new post (new window)',
+                  className: 'insignificant',
+                  keys: (
+                    <>
+                      <kbd>Shift</kbd> + <kbd>c</kbd>
+                    </>
+                  ),
+                },
                 {
                   action: 'Send post',
                   keys: (
@@ -134,6 +143,15 @@ export default memo(function KeyboardShortcutsHelp() {
                   action: 'Reply',
                   keys: <kbd>r</kbd>,
                 },
+                {
+                  action: 'Reply (new window)',
+                  className: 'insignificant',
+                  keys: (
+                    <>
+                      <kbd>Shift</kbd> + <kbd>r</kbd>
+                    </>
+                  ),
+                },
                 {
                   action: 'Like (favourite)',
                   keys: (
@@ -154,9 +172,17 @@ export default memo(function KeyboardShortcutsHelp() {
                   action: 'Bookmark',
                   keys: <kbd>d</kbd>,
                 },
-              ].map(({ action, keys }) => (
+                {
+                  action: 'Toggle Cloak mode',
+                  keys: (
+                    <>
+                      <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>k</kbd>
+                    </>
+                  ),
+                },
+              ].map(({ action, className, keys }) => (
                 <tr key={action}>
-                  <th>{action}</th>
+                  <th class={className}>{action}</th>
                   <td>{keys}</td>
                 </tr>
               ))}
diff --git a/src/components/media-alt-modal.jsx b/src/components/media-alt-modal.jsx
index a45a9c83..58908013 100644
--- a/src/components/media-alt-modal.jsx
+++ b/src/components/media-alt-modal.jsx
@@ -57,6 +57,7 @@ export default function MediaAltModal({ alt, lang, onClose }) {
         <p
           style={{
             whiteSpace: 'pre-wrap',
+            textWrap: 'pretty',
           }}
         >
           {alt}
diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx
index 3c920cc2..74cf21cf 100644
--- a/src/components/media-modal.jsx
+++ b/src/components/media-modal.jsx
@@ -103,7 +103,9 @@ function MediaModal({
   }, []);
 
   return (
-    <div class="media-modal-container">
+    <div
+      class={`media-modal-container media-modal-count-${mediaAttachments?.length}`}
+    >
       <div
         ref={carouselRef}
         tabIndex="0"
@@ -142,7 +144,11 @@ function MediaModal({
               key={media.id}
               ref={i === currentIndex ? carouselFocusItem : null}
               onClick={(e) => {
-                if (e.target !== e.currentTarget) {
+                // console.log(e);
+                // if (e.target !== e.currentTarget) {
+                //   setShowControls(!showControls);
+                // }
+                if (!e.target.classList.contains('media')) {
                   setShowControls(!showControls);
                 }
               }}
@@ -248,7 +254,7 @@ function MediaModal({
             //   }
             // }}
           >
-            <span class="button-label">See post </span>&raquo;
+            <span class="button-label">View post </span>&raquo;
           </Link>
         </span>
       </div>
diff --git a/src/components/media-post.css b/src/components/media-post.css
new file mode 100644
index 00000000..d8c12673
--- /dev/null
+++ b/src/components/media-post.css
@@ -0,0 +1,107 @@
+.media-post {
+  --item-radius: 16px;
+  position: relative;
+  animation: appear-smooth 1s ease-out;
+
+  &:is(.filtered, .has-spoiler) :is(img, video) {
+    filter: blur(32px);
+    image-rendering: crisp-edges;
+    image-rendering: pixelated;
+    animation: none !important;
+  }
+
+  &.filtered[data-filtered-text]:before {
+    content: attr(data-filtered-text);
+  }
+  &.has-spoiler[data-spoiler-text]:before {
+    content: attr(data-spoiler-text);
+  }
+
+  &.filtered[data-filtered-text]:before,
+  &.has-spoiler[data-spoiler-text]:before {
+    pointer-events: none;
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1;
+    background-color: var(--bg-blur-color);
+    margin: 8px;
+    padding: 4px 6px;
+    border-radius: calc(var(--item-radius) / 2);
+    font-size: 90%;
+    border: var(--hairline-width) dashed var(--bg-color);
+    word-break: break-word;
+    word-wrap: break-word;
+    overflow-wrap: break-word;
+    mix-blend-mode: luminosity;
+    -webkit-line-clamp: 3;
+    line-clamp: 3;
+    -webkit-box-orient: vertical;
+    box-orient: vertical;
+    display: -webkit-box;
+    display: box;
+    overflow: hidden;
+    z-index: 2;
+
+    > * {
+      pointer-events: none;
+    }
+  }
+
+  .media {
+    border-radius: var(--item-radius);
+    overflow: hidden;
+    position: relative;
+    display: block;
+    aspect-ratio: 1 !important;
+
+    &:before {
+      position: absolute;
+      inset: 0;
+      content: '';
+      border: 1px solid var(--outline-color);
+      border-radius: inherit;
+    }
+
+    &:not(.media-audio) {
+      background-color: var(--average-color, var(--media-bg-color));
+    }
+
+    @media (hover: hover) {
+      &:hover {
+        --drop-shadow: var(--drop-shadow-color);
+        position: relative;
+        z-index: 1;
+        box-shadow: 0 8px 16px -4px var(--drop-shadow),
+          0 4px 8px var(--drop-shadow);
+
+        @media (prefers-color-scheme: dark) {
+          --drop-shadow: var(--link-color);
+        }
+      }
+    }
+
+    &:active:not(:has(button:active)) {
+      box-shadow: none;
+      filter: brightness(0.8);
+      transform: scale(0.99);
+    }
+
+    video,
+    img,
+    audio {
+      border-radius: 16px;
+      /* object-fit: scale-down; */
+      object-fit: cover;
+      width: 100%;
+      height: 100%;
+      vertical-align: top;
+    }
+
+    :not(.filtered, .has-spoiler) &:is(:hover, :focus) img {
+      /* Less delay here to make it feel more responsive */
+      animation: position-object 5s ease-in-out 0.1s 5;
+      animation-duration: var(--anim-duration, 5s);
+    }
+  }
+}
diff --git a/src/components/media-post.jsx b/src/components/media-post.jsx
new file mode 100644
index 00000000..fdc1040c
--- /dev/null
+++ b/src/components/media-post.jsx
@@ -0,0 +1,150 @@
+import './media-post.css';
+
+import { memo } from 'preact/compat';
+import { useContext, useMemo } from 'preact/hooks';
+import { useSnapshot } from 'valtio';
+
+import FilterContext from '../utils/filter-context';
+import { isFiltered } from '../utils/filters';
+import states, { statusKey } from '../utils/states';
+import store from '../utils/store';
+
+import Media from './media';
+
+function MediaPost({
+  class: className,
+  statusID,
+  status,
+  instance,
+  parent,
+  // allowFilters,
+  onMediaClick,
+}) {
+  let sKey = statusKey(statusID, instance);
+  const snapStates = useSnapshot(states);
+  if (!status) {
+    status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
+    sKey = statusKey(status?.id, instance);
+  }
+  if (!status) {
+    return null;
+  }
+
+  const {
+    account: {
+      acct,
+      avatar,
+      avatarStatic,
+      id: accountId,
+      url: accountURL,
+      displayName,
+      username,
+      emojis: accountEmojis,
+      bot,
+      group,
+    },
+    id,
+    repliesCount,
+    reblogged,
+    reblogsCount,
+    favourited,
+    favouritesCount,
+    bookmarked,
+    poll,
+    muted,
+    sensitive,
+    spoilerText,
+    visibility, // public, unlisted, private, direct
+    language,
+    editedAt,
+    filtered,
+    card,
+    createdAt,
+    inReplyToId,
+    inReplyToAccountId,
+    content,
+    mentions,
+    mediaAttachments,
+    reblog,
+    uri,
+    url,
+    emojis,
+    // Non-API props
+    _deleted,
+    _pinned,
+    // _filtered,
+  } = status;
+
+  if (!mediaAttachments?.length) {
+    return null;
+  }
+
+  const debugHover = (e) => {
+    if (e.shiftKey) {
+      console.log({
+        ...status,
+      });
+    }
+  };
+
+  const currentAccount = useMemo(() => {
+    return store.session.get('currentAccount');
+  }, []);
+  const isSelf = useMemo(() => {
+    return currentAccount && currentAccount === accountId;
+  }, [accountId, currentAccount]);
+
+  const filterContext = useContext(FilterContext);
+  const filterInfo = !isSelf && isFiltered(filtered, filterContext);
+
+  if (filterInfo?.action === 'hide') {
+    return null;
+  }
+
+  console.debug('RENDER Media post', id, status?.account.displayName);
+
+  // const readingExpandSpoilers = useMemo(() => {
+  //   const prefs = store.account.get('preferences') || {};
+  //   return !!prefs['reading:expand:spoilers'];
+  // }, []);
+  const hasSpoiler = spoilerText || sensitive;
+
+  const Parent = parent || 'div';
+
+  return mediaAttachments.map((media, i) => {
+    const mediaKey = `${sKey}-${media.id}`;
+    const filterTitleStr = filterInfo?.titlesStr;
+    return (
+      <Parent
+        data-state-post-id={sKey}
+        onMouseEnter={debugHover}
+        key={mediaKey}
+        data-spoiler-text={
+          spoilerText || (sensitive ? 'Sensitive media' : undefined)
+        }
+        data-filtered-text={
+          filterInfo
+            ? `Filtered${filterTitleStr ? `: ${filterTitleStr}` : ''}`
+            : undefined
+        }
+        class={`
+          media-post
+          ${filterInfo ? 'filtered' : ''}
+          ${hasSpoiler ? 'has-spoiler' : ''}
+        `}
+      >
+        <Media
+          class={className}
+          media={media}
+          lang={language}
+          to={`/${instance}/s/${id}?media-only=${i + 1}`}
+          onClick={
+            onMediaClick ? (e) => onMediaClick(e, i, media, status) : undefined
+          }
+        />
+      </Parent>
+    );
+  });
+}
+
+export default memo(MediaPost);
diff --git a/src/components/media.jsx b/src/components/media.jsx
index 590caa40..f0390458 100644
--- a/src/components/media.jsx
+++ b/src/components/media.jsx
@@ -62,6 +62,7 @@ export const isMediaCaptionLong = mem((caption) =>
 );
 
 function Media({
+  class: className = '',
   media,
   to,
   lang,
@@ -170,6 +171,9 @@ function Media({
   const maxAspectHeight =
     window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
   const maxHeight = orientation === 'portrait' ? 0 : 160;
+  const averageColorStyle = {
+    '--average-color': rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
+  };
   const mediaStyles =
     width && height
       ? {
@@ -180,8 +184,11 @@ function Media({
             (width / height) * Math.max(maxHeight, maxAspectHeight)
           }px`,
           aspectRatio: `${width} / ${height}`,
+          ...averageColorStyle,
         }
-      : {};
+      : {
+          ...averageColorStyle,
+        };
 
   const longDesc = isMediaCaptionLong(description);
   const showInlineDesc =
@@ -233,7 +240,7 @@ function Media({
       <Figure>
         <Parent
           ref={parentRef}
-          class={`media media-image`}
+          class={`media media-image ${className}`}
           onClick={onClick}
           data-orientation={orientation}
           data-has-alt={!showInlineDesc}
@@ -244,6 +251,7 @@ function Media({
                   backgroundSize: imageSmallerThanParent
                     ? `${width}px ${height}px`
                     : undefined,
+                  ...averageColorStyle,
                 }
               : mediaStyles
           }
@@ -341,11 +349,13 @@ function Media({
     return (
       <Figure>
         <Parent
-          class={`media media-${isGIF ? 'gif' : 'video'} ${
+          class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
             autoGIFAnimate ? 'media-contain' : ''
           }`}
           data-orientation={orientation}
-          data-formatted-duration={formattedDuration}
+          data-formatted-duration={
+            !showOriginal ? formattedDuration : undefined
+          }
           data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
           data-has-alt={!showInlineDesc}
           // style={{
@@ -448,8 +458,10 @@ function Media({
     return (
       <Figure>
         <Parent
-          class="media media-audio"
-          data-formatted-duration={formattedDuration}
+          class={`media media-audio ${className}`}
+          data-formatted-duration={
+            !showOriginal ? formattedDuration : undefined
+          }
           data-has-alt={!showInlineDesc}
           onClick={onClick}
           style={!showOriginal && mediaStyles}
diff --git a/src/components/nav-menu.jsx b/src/components/nav-menu.jsx
index 2e22d532..96b9ce46 100644
--- a/src/components/nav-menu.jsx
+++ b/src/components/nav-menu.jsx
@@ -35,8 +35,9 @@ function NavMenu(props) {
   // User may choose pin or not to pin Following
   // If user doesn't pin Following, we show it in the menu
   const showFollowing =
-    (snapStates.settings.shortcutsColumnsMode ||
-      snapStates.settings.shortcutsViewMode === 'multi-column') &&
+    (snapStates.settings.shortcutsViewMode === 'multi-column' ||
+      (!snapStates.settings.shortcutsViewMode &&
+        snapStates.settings.shortcutsColumnsMode)) &&
     !snapStates.shortcuts.find((pin) => pin.type === 'following');
 
   const bindLongPress = useLongPress(
diff --git a/src/components/notification.jsx b/src/components/notification.jsx
index 198c542e..823d9f43 100644
--- a/src/components/notification.jsx
+++ b/src/components/notification.jsx
@@ -1,3 +1,4 @@
+import { Fragment } from 'preact';
 import { memo } from 'preact/compat';
 
 import shortenNumber from '../utils/shorten-number';
@@ -221,9 +222,10 @@ function Notification({ notification, instance, isStatic }) {
         )}
         {_accounts?.length > 1 && (
           <p class="avatars-stack">
-            {_accounts.slice(0, AVATARS_LIMIT).map((account, i) => (
-              <>
+            {_accounts.slice(0, AVATARS_LIMIT).map((account) => (
+              <Fragment key={account.id}>
                 <a
+                  key={account.id}
                   href={account.url}
                   rel="noopener noreferrer"
                   class="account-avatar-stack"
@@ -261,7 +263,7 @@ function Notification({ notification, instance, isStatic }) {
                     </div>
                   )}
                 </a>{' '}
-              </>
+              </Fragment>
             ))}
             <button
               type="button"
diff --git a/src/components/shortcuts-settings.css b/src/components/shortcuts-settings.css
index 086c7911..1c9aa7b1 100644
--- a/src/components/shortcuts-settings.css
+++ b/src/components/shortcuts-settings.css
@@ -86,7 +86,8 @@
   transition: all 0.2s ease-out;
 }
 #shortcuts-settings-container .shortcuts-view-mode label.checked {
-  box-shadow: inset 0 0 0 3px var(--link-color);
+  box-shadow: inset 0 0 0 3px var(--link-color),
+    inset 0 0 32px var(--link-faded-color);
 }
 #shortcuts-settings-container
   .shortcuts-view-mode
diff --git a/src/components/shortcuts-settings.jsx b/src/components/shortcuts-settings.jsx
index 11b0b895..9d5bb516 100644
--- a/src/components/shortcuts-settings.jsx
+++ b/src/components/shortcuts-settings.jsx
@@ -22,7 +22,7 @@ import Icon from './icon';
 import MenuConfirm from './menu-confirm';
 import Modal from './modal';
 
-const SHORTCUTS_LIMIT = 9;
+export const SHORTCUTS_LIMIT = 9;
 
 const TYPES = [
   'following',
@@ -104,6 +104,11 @@ const TYPE_PARAMS = {
       placeholder: 'e.g. PixelArt (Max 5, space-separated)',
       pattern: '[^#]+',
     },
+    {
+      text: 'Media only',
+      name: 'media',
+      type: 'checkbox',
+    },
     {
       text: 'Instance',
       name: 'instance',
@@ -113,6 +118,14 @@ const TYPE_PARAMS = {
     },
   ],
 };
+const fetchListTitle = pmem(async ({ id }) => {
+  const list = await api().masto.v1.lists.$select(id).fetch();
+  return list.title;
+});
+const fetchAccountTitle = pmem(async ({ id }) => {
+  const account = await api().masto.v1.accounts.$select(id).fetch();
+  return account.username || account.acct || account.displayName;
+});
 export const SHORTCUTS_META = {
   following: {
     id: 'home',
@@ -134,10 +147,7 @@ export const SHORTCUTS_META = {
   },
   list: {
     id: 'list',
-    title: pmem(async ({ id }) => {
-      const list = await api().masto.v1.lists.$select(id).fetch();
-      return list.title;
-    }),
+    title: fetchListTitle,
     path: ({ id }) => `/l/${id}`,
     icon: 'list',
   },
@@ -163,10 +173,7 @@ export const SHORTCUTS_META = {
   },
   'account-statuses': {
     id: 'account-statuses',
-    title: pmem(async ({ id }) => {
-      const account = await api().masto.v1.accounts.$select(id).fetch();
-      return account.username || account.acct || account.displayName;
-    }),
+    title: fetchAccountTitle,
     path: ({ id }) => `/a/${id}`,
     icon: 'user',
   },
@@ -186,51 +193,22 @@ export const SHORTCUTS_META = {
     id: 'hashtag',
     title: ({ hashtag }) => hashtag,
     subtitle: ({ instance }) => instance || api().instance,
-    path: ({ hashtag, instance }) =>
-      `${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}`,
+    path: ({ hashtag, instance, media }) =>
+      `${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}${
+        media ? '?media=1' : ''
+      }`,
     icon: 'hashtag',
   },
 };
 
 function ShortcutsSettings({ onClose }) {
   const snapStates = useSnapshot(states);
-  const { masto } = api();
   const { shortcuts } = snapStates;
-
-  const [lists, setLists] = useState([]);
-  const [followedHashtags, setFollowedHashtags] = useState([]);
   const [showForm, setShowForm] = useState(false);
   const [showImportExport, setShowImportExport] = useState(false);
 
   const [shortcutsListParent] = useAutoAnimate();
 
-  useEffect(() => {
-    (async () => {
-      try {
-        const lists = await masto.v1.lists.list();
-        lists.sort((a, b) => a.title.localeCompare(b.title));
-        setLists(lists);
-      } catch (e) {
-        console.error(e);
-      }
-    })();
-
-    (async () => {
-      try {
-        const iterator = masto.v1.followedTags.list();
-        const tags = [];
-        do {
-          const { value, done } = await iterator.next();
-          if (done || value?.length === 0) break;
-          tags.push(...value);
-        } while (true);
-        setFollowedHashtags(tags);
-      } catch (e) {
-        console.error(e);
-      }
-    })();
-  }, []);
-
   return (
     <div id="shortcuts-settings-container" class="sheet" tabindex="-1">
       {!!onClose && (
@@ -293,33 +271,6 @@ function ShortcutsSettings({ onClose }) {
             );
           })}
         </div>
-        {/* <select
-              value={snapStates.settings.shortcutsViewMode || 'float-button'}
-              onChange={(e) => {
-                states.settings.shortcutsViewMode = e.target.value;
-              }}
-            >
-              <option value="float-button">Floating button</option>
-              <option value="multi-column">Multi-column</option>
-              <option value="tab-menu-bar">Tab/Menu bar </option>
-            </select> */}
-        {/* <p>
-          <details>
-            <summary class="insignificant">
-              Experimental Multi-column mode
-            </summary>
-            <label>
-              <input
-                type="checkbox"
-                checked={snapStates.settings.shortcutsColumnsMode}
-                onChange={(e) => {
-                  states.settings.shortcutsColumnsMode = e.target.checked;
-                }}
-              />{' '}
-              Show shortcuts in multiple columns instead of the floating button.
-            </label>
-          </details>
-        </p> */}
         {shortcuts.length > 0 ? (
           <ol class="shortcuts-list" ref={shortcutsListParent}>
             {shortcuts.filter(Boolean).map((shortcut, i) => {
@@ -474,8 +425,6 @@ function ShortcutsSettings({ onClose }) {
           <ShortcutForm
             shortcut={showForm.shortcut}
             shortcutIndex={showForm.shortcutIndex}
-            lists={lists}
-            followedHashtags={followedHashtags}
             onSubmit={({ result, mode }) => {
               console.log('onSubmit', result);
               if (mode === 'edit') {
@@ -507,9 +456,27 @@ function ShortcutsSettings({ onClose }) {
   );
 }
 
+const FETCH_MAX_AGE = 1000 * 60; // 1 minute
+const fetchLists = pmem(
+  () => {
+    const { masto } = api();
+    return masto.v1.lists.list();
+  },
+  {
+    maxAge: FETCH_MAX_AGE,
+  },
+);
+const fetchFollowedHashtags = pmem(
+  () => {
+    const { masto } = api();
+    return masto.v1.followedTags.list();
+  },
+  {
+    maxAge: FETCH_MAX_AGE,
+  },
+);
+
 function ShortcutForm({
-  lists,
-  followedHashtags,
   onSubmit,
   disabled,
   shortcut,
@@ -520,6 +487,41 @@ function ShortcutForm({
   const editMode = !!shortcut;
   const [currentType, setCurrentType] = useState(shortcut?.type || null);
 
+  const [uiState, setUIState] = useState('default');
+  const [lists, setLists] = useState([]);
+  const [followedHashtags, setFollowedHashtags] = useState([]);
+  useEffect(() => {
+    (async () => {
+      if (currentType !== 'list') return;
+      try {
+        setUIState('loading');
+        const lists = await fetchLists();
+        lists.sort((a, b) => a.title.localeCompare(b.title));
+        setLists(lists);
+        setUIState('default');
+      } catch (e) {
+        console.error(e);
+        setUIState('error');
+      }
+    })();
+
+    (async () => {
+      if (currentType !== 'hashtag') return;
+      try {
+        const iterator = fetchFollowedHashtags();
+        const tags = [];
+        do {
+          const { value, done } = await iterator.next();
+          if (done || value?.length === 0) break;
+          tags.push(...value);
+        } while (true);
+        setFollowedHashtags(tags);
+      } catch (e) {
+        console.error(e);
+      }
+    })();
+  }, [currentType]);
+
   const formRef = useRef();
   useEffect(() => {
     if (editMode && currentType && TYPE_PARAMS[currentType]) {
@@ -608,7 +610,8 @@ function ShortcutForm({
                       <select
                         name="id"
                         required={!notRequired}
-                        disabled={disabled}
+                        disabled={disabled || uiState === 'loading'}
+                        defaultValue={editMode ? shortcut.id : undefined}
                       >
                         {lists.map((list) => (
                           <option value={list.id}>{list.title}</option>
@@ -653,7 +656,11 @@ function ShortcutForm({
             },
           )}
           <footer>
-            <button type="submit" class="block" disabled={disabled}>
+            <button
+              type="submit"
+              class="block"
+              disabled={disabled || uiState === 'loading'}
+            >
               {editMode ? 'Save' : 'Add'}
             </button>
             {editMode && (
diff --git a/src/components/shortcuts.css b/src/components/shortcuts.css
index a0e88160..59bee8f7 100644
--- a/src/components/shortcuts.css
+++ b/src/components/shortcuts.css
@@ -152,7 +152,7 @@ shortcuts .tab-bar[hidden] {
   }
 }
 
-@media (min-width: 40em) {
+@media (min-width: 40em) and (hover: hover) {
   #app[data-shortcuts-view-mode='tab-menu-bar'] .timeline-deck {
     margin-top: 44px;
   }
diff --git a/src/components/shortcuts.jsx b/src/components/shortcuts.jsx
index b45a2354..e21f9bef 100644
--- a/src/components/shortcuts.jsx
+++ b/src/components/shortcuts.jsx
@@ -25,8 +25,8 @@ function Shortcuts() {
     return null;
   }
   if (
-    settings.shortcutsColumnsMode ||
-    settings.shortcutsViewMode === 'multi-column'
+    settings.shortcutsViewMode === 'multi-column' ||
+    (!settings.shortcutsViewMode && settings.shortcutsColumnsMode)
   ) {
     return null;
   }
@@ -90,7 +90,13 @@ function Shortcuts() {
   return (
     <div id="shortcuts">
       {snapStates.settings.shortcutsViewMode === 'tab-menu-bar' ? (
-        <nav class="tab-bar">
+        <nav
+          class="tab-bar"
+          onContextMenu={(e) => {
+            e.preventDefault();
+            states.showShortcutsSettings = true;
+          }}
+        >
           <ul>
             {formattedShortcuts.map(
               ({ id, path, title, subtitle, icon }, i) => {
@@ -146,6 +152,10 @@ function Shortcuts() {
               type="button"
               id="shortcuts-button"
               class="plain"
+              onContextMenu={(e) => {
+                e.preventDefault();
+                states.showShortcutsSettings = true;
+              }}
               onTransitionStart={(e) => {
                 // Close menu if the button disappears
                 try {
diff --git a/src/components/status.css b/src/components/status.css
index 77f55d9a..0fd1890b 100644
--- a/src/components/status.css
+++ b/src/components/status.css
@@ -603,6 +603,7 @@
   margin-block: min(0.75em, 12px);
   white-space: pre-wrap;
   tab-size: 2;
+  text-wrap: pretty;
 }
 .status .content p:first-child {
   margin-block-start: 0;
@@ -726,6 +727,7 @@
       white-space: pre-line;
       flex-basis: 15em;
       flex-grow: 1;
+      text-wrap: pretty;
     }
   }
 
@@ -846,7 +848,7 @@
   object-fit: cover;
   vertical-align: middle;
 }
-.status .media {
+:is(.status, .media-post) .media {
   cursor: pointer;
 
   &[data-has-alt] {
@@ -885,7 +887,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
   position: relative;
   background-clip: padding-box;
 }
-.status :is(.media-video, .media-audio) .media-play {
+:is(.status, .media-post) :is(.media-video, .media-audio) .media-play {
   pointer-events: none;
   width: 44px;
   height: 44px;
@@ -902,10 +904,13 @@ body:has(#modal-container .carousel) .status .media img:hover {
   border-radius: 70px;
   transition: transform 0.2s ease-in-out;
 }
-.status :is(.media-video, .media-audio):hover:not(:active) .media-play {
+:is(.status, .media-post)
+  :is(.media-video, .media-audio):hover:not(:active)
+  .media-play {
   transform: translate(-50%, -50%) scale(1.1);
 }
-.status :is(.media-video, .media-audio)[data-formatted-duration]:after {
+:is(.status, .media-post)
+  :is(.media-video, .media-audio)[data-formatted-duration]:after {
   font-size: 12px;
   pointer-events: none;
   content: attr(data-formatted-duration);
@@ -918,10 +923,10 @@ body:has(#modal-container .carousel) .status .media img:hover {
   border-radius: 4px;
   padding: 0 4px;
 }
-.status .media-audio[data-formatted-duration]:after {
+:is(.status, .media-post) .media-audio[data-formatted-duration]:after {
   content: '♬ ' attr(data-formatted-duration);
 }
-.status .media-gif[data-label]:not(:hover):after {
+:is(.status, .media-post) .media-gif[data-label]:not(:hover):after {
   font-size: 12px;
   font-weight: bold;
   pointer-events: none;
@@ -953,12 +958,13 @@ body:has(#modal-container .carousel) .status .media img:hover {
 .status .media-audio audio {
   width: 100%;
 } */
-.status .media-audio {
+:is(.status, .media-post) .media-audio {
   width: 100%;
   height: 100%;
+  min-height: 88px;
   background-image: radial-gradient(
       circle at center center,
-      var(--bg-color),
+      transparent,
       var(--bg-faded-color)
     ),
     repeating-radial-gradient(
@@ -1078,7 +1084,6 @@ body:has(#modal-container .carousel) .status .media img:hover {
   font-size: 90%;
   z-index: 1;
   text-shadow: 0 var(--hairline-width) var(--bg-color);
-  mix-blend-mode: luminosity;
   white-space: pre-line;
 
   &:is(:hover, :focus) {
@@ -1202,6 +1207,7 @@ a:focus-visible .card img {
   box-orient: vertical;
   -webkit-line-clamp: 2;
   line-clamp: 2;
+  text-wrap: balance;
 }
 .card .meta {
   font-size: smaller;
diff --git a/src/components/status.jsx b/src/components/status.jsx
index 565901f6..a5ef587c 100644
--- a/src/components/status.jsx
+++ b/src/components/status.jsx
@@ -13,6 +13,7 @@ import pThrottle from 'p-throttle';
 import { memo } from 'preact/compat';
 import {
   useCallback,
+  useContext,
   useEffect,
   useMemo,
   useRef,
@@ -34,6 +35,8 @@ import Poll from '../components/poll';
 import { api } from '../utils/api';
 import emojifyText from '../utils/emojify-text';
 import enhanceContent from '../utils/enhance-content';
+import FilterContext from '../utils/filter-context';
+import { isFiltered } from '../utils/filters';
 import getTranslateTargetLanguage from '../utils/get-translate-target-language';
 import getHTMLText from '../utils/getHTMLText';
 import handleContentLinks from '../utils/handle-content-links';
@@ -41,6 +44,7 @@ import htmlContentLength from '../utils/html-content-length';
 import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
 import localeMatch from '../utils/locale-match';
 import niceDateTime from '../utils/nice-date-time';
+import openCompose from '../utils/open-compose';
 import pmem from '../utils/pmem';
 import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
 import shortenNumber from '../utils/shorten-number';
@@ -78,6 +82,10 @@ const visibilityText = {
   direct: 'Private mention',
 };
 
+const isIOS =
+  window.ontouchstart !== undefined &&
+  /iPad|iPhone|iPod/.test(navigator.userAgent);
+
 function Status({
   statusID,
   status,
@@ -90,7 +98,7 @@ function Status({
   enableTranslate,
   forceTranslate: _forceTranslate,
   previewMode,
-  allowFilters,
+  // allowFilters,
   onMediaClick,
   quoted,
   onStatusLinkClick = () => {},
@@ -166,9 +174,24 @@ function Status({
     // Non-API props
     _deleted,
     _pinned,
-    _filtered,
+    // _filtered,
   } = status;
 
+  const currentAccount = useMemo(() => {
+    return store.session.get('currentAccount');
+  }, []);
+  const isSelf = useMemo(() => {
+    return currentAccount && currentAccount === accountId;
+  }, [accountId, currentAccount]);
+
+  const filterContext = useContext(FilterContext);
+  const filterInfo =
+    !isSelf && !readOnly && !previewMode && isFiltered(filtered, filterContext);
+
+  if (filterInfo?.action === 'hide') {
+    return null;
+  }
+
   console.debug('RENDER Status', id, status?.account.displayName, quoted);
 
   const debugHover = (e) => {
@@ -179,11 +202,11 @@ function Status({
     }
   };
 
-  if (allowFilters && size !== 'l' && _filtered) {
+  if (/*allowFilters && */ size !== 'l' && filterInfo) {
     return (
       <FilteredStatus
         status={status}
-        filterInfo={_filtered}
+        filterInfo={filterInfo}
         instance={instance}
         containerProps={{
           onMouseEnter: debugHover,
@@ -195,13 +218,6 @@ function Status({
   const createdAtDate = new Date(createdAt);
   const editedAtDate = new Date(editedAt);
 
-  const currentAccount = useMemo(() => {
-    return store.session.get('currentAccount');
-  }, []);
-  const isSelf = useMemo(() => {
-    return currentAccount && currentAccount === accountId;
-  }, [accountId, currentAccount]);
-
   let inReplyToAccountRef = mentions?.find(
     (mention) => mention.id === inReplyToAccountId,
   );
@@ -238,7 +254,11 @@ function Status({
 
     if (group) {
       return (
-        <div class="status-group" onMouseEnter={debugHover}>
+        <div
+          data-state-post-id={sKey}
+          class="status-group"
+          onMouseEnter={debugHover}
+        >
           <div class="status-pre-meta">
             <Icon icon="group" size="l" alt="Group" />{' '}
             <NameText account={status.account} instance={instance} showAvatar />
@@ -249,13 +269,18 @@ function Status({
             instance={instance}
             size={size}
             contentTextWeight={contentTextWeight}
+            readOnly={readOnly}
           />
         </div>
       );
     }
 
     return (
-      <div class="status-reblog" onMouseEnter={debugHover}>
+      <div
+        data-state-post-id={sKey}
+        class="status-reblog"
+        onMouseEnter={debugHover}
+      >
         <div class="status-pre-meta">
           <Icon icon="rocket" size="l" />{' '}
           <NameText account={status.account} instance={instance} showAvatar />{' '}
@@ -267,6 +292,7 @@ function Status({
           instance={instance}
           size={size}
           contentTextWeight={contentTextWeight}
+          readOnly={readOnly}
         />
       </div>
     );
@@ -348,10 +374,17 @@ function Status({
     canBoost = true;
   }
 
-  const replyStatus = () => {
+  const replyStatus = (e) => {
     if (!sameInstance || !authenticated) {
       return alert(unauthInteractionErrorMessage);
     }
+    // syntheticEvent comes from MenuItem
+    if (e?.shiftKey || e?.syntheticEvent?.shiftKey) {
+      const newWin = openCompose({
+        replyToStatus: status,
+      });
+      if (newWin) return;
+    }
     states.showCompose = {
       replyToStatus: status,
     };
@@ -795,13 +828,13 @@ function Status({
   const contextMenuRef = useRef();
   const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
   const [contextMenuProps, setContextMenuProps] = useState({});
-  const isIOS =
-    window.ontouchstart !== undefined &&
-    /iPad|iPhone|iPod/.test(navigator.userAgent);
+
+  const showContextMenu = !isSizeLarge && !previewMode && !_deleted && !quoted;
+
   // Only iOS/iPadOS browsers don't support contextmenu
   // Some comments report iPadOS might support contextmenu if a mouse is connected
   const bindLongPressContext = useLongPress(
-    isIOS
+    isIOS && showContextMenu
       ? (e) => {
           if (e.pointerType === 'mouse') return;
           // There's 'pen' too, but not sure if contextmenu event would trigger from a pen
@@ -829,10 +862,8 @@ function Status({
     },
   );
 
-  const showContextMenu = size !== 'l' && !previewMode && !_deleted && !quoted;
-
   const hotkeysEnabled = !readOnly && !previewMode;
-  const rRef = useHotkeys('r', replyStatus, {
+  const rRef = useHotkeys('r, shift+r', replyStatus, {
     enabled: hotkeysEnabled,
   });
   const fRef = useHotkeys(
@@ -960,6 +991,7 @@ function Status({
 
   return (
     <article
+      data-state-post-id={sKey}
       ref={(node) => {
         statusRef.current = node;
         // Use parent node if it's in focus
@@ -1091,6 +1123,15 @@ function Status({
               <Link
                 to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
                 onClick={(e) => {
+                  if (
+                    e.metaKey ||
+                    e.ctrlKey ||
+                    e.shiftKey ||
+                    e.altKey ||
+                    e.which === 2
+                  ) {
+                    return;
+                  }
                   e.preventDefault();
                   e.stopPropagation();
                   onStatusLinkClick?.(e, status);
@@ -1457,6 +1498,7 @@ function Status({
                       {' '}
                       &bull; <Icon icon="pencil" alt="Edited" />{' '}
                       <time
+                        tabIndex="0"
                         class="edited"
                         datetime={editedAtDate.toISOString()}
                         onClick={() => {
@@ -1576,10 +1618,11 @@ function Status({
       </div>
       {!!showEdited && (
         <Modal
+          class="light"
           onClick={(e) => {
             if (e.target === e.currentTarget) {
               setShowEdited(false);
-              statusRef.current?.focus();
+              // statusRef.current?.focus();
             }
           }}
         >
@@ -1691,7 +1734,9 @@ function Card({ card, instance }) {
   if (snapStates.unfurledLinks[url]) return null;
 
   if (hasText && (image || (type === 'photo' && blurhash))) {
-    const domain = new URL(url).hostname.replace(/^www\./, '');
+    const domain = new URL(url).hostname
+      .replace(/^www\./, '')
+      .replace(/\/$/, '');
     let blurhashImage;
     if (!image) {
       const w = 44;
@@ -1737,7 +1782,10 @@ function Card({ card, instance }) {
             {title}
           </p>
           <p class="meta" dir="auto">
-            {description || providerName || authorName}
+            {description ||
+              (!!publishedAt && (
+                <RelativeTime datetime={publishedAt} format="micro" />
+              ))}
           </p>
         </div>
       </a>
@@ -2120,10 +2168,24 @@ function _unfurlMastodonLink(instance, url) {
 
   let remoteInstanceFetch;
   let theURL = url;
-  if (/\/\/elk\.[^\/]+\/[^.]+\.[^.]+/i.test(theURL)) {
-    // E.g. https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
+
+  // https://elk.zone/domain.com/@stest/123 -> https://domain.com/@stest/123
+  if (/\/\/elk\.[^\/]+\/[^\/]+\.[^\/]+/i.test(theURL)) {
     theURL = theURL.replace(/elk\.[^\/]+\//i, '');
   }
+
+  // https://trunks.social/status/domain.com/@stest/123 -> https://domain.com/@stest/123
+  if (/\/\/trunks\.[^\/]+\/status\/[^\/]+\.[^\/]+/i.test(theURL)) {
+    theURL = theURL.replace(/trunks\.[^\/]+\/status\//i, '');
+  }
+
+  // https://phanpy.social/#/domain.com/s/123 -> https://domain.com/statuses/123
+  if (/\/#\/[^\/]+\.[^\/]+\/s\/.+/i.test(theURL)) {
+    const urlAfterHash = theURL.split('/#/')[1];
+    const finalURL = urlAfterHash.replace(/\/s\//i, '/@fakeUsername/');
+    theURL = `https://${finalURL}`;
+  }
+
   const urlObj = new URL(theURL);
   const domain = urlObj.hostname;
   const path = urlObj.pathname;
@@ -2151,7 +2213,7 @@ function _unfurlMastodonLink(instance, url) {
   const { masto } = api({ instance });
   const mastoSearchFetch = masto.v2.search
     .fetch({
-      q: url,
+      q: theURL,
       type: 'statuses',
       resolve: true,
       limit: 1,
@@ -2224,6 +2286,7 @@ const unfurlMastodonLink = throttle(_unfurlMastodonLink);
 
 function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
   const {
+    id: statusID,
     account: { avatar, avatarStatic, bot, group },
     createdAt,
     visibility,
@@ -2248,6 +2311,15 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
   );
 
   const statusPeekRef = useTruncated();
+  const sKey =
+    statusKey(status.id, instance) +
+    ' ' +
+    (statusKey(reblog?.id, instance) || '');
+
+  const actualStatusID = reblog?.id || statusID;
+  const url = instance
+    ? `/${instance}/s/${actualStatusID}`
+    : `/s/${actualStatusID}`;
 
   return (
     <div
@@ -2260,7 +2332,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
       }}
       {...bindLongPressPeek()}
     >
-      <article class="status filtered" tabindex="-1">
+      <article data-state-post-id={sKey} class="status filtered" tabindex="-1">
         <b
           class="status-filtered-badge clickable badge-meta"
           title={filterTitleStr}
@@ -2324,7 +2396,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
               <Link
                 ref={statusPeekRef}
                 class="status-link"
-                to={`/${instance}/s/${status.id}`}
+                to={url}
                 onClick={() => {
                   setShowPeek(false);
                 }}
diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx
index a687ab43..6aab086b 100644
--- a/src/components/timeline.jsx
+++ b/src/components/timeline.jsx
@@ -4,6 +4,8 @@ import { InView } from 'react-intersection-observer';
 import { useDebouncedCallback } from 'use-debounce';
 import { useSnapshot } from 'valtio';
 
+import FilterContext from '../utils/filter-context';
+import { isFiltered } from '../utils/filters';
 import states, { statusKey } from '../utils/states';
 import statusPeek from '../utils/status-peek';
 import { groupBoosts, groupContext } from '../utils/timeline-utils';
@@ -13,6 +15,7 @@ import useScroll from '../utils/useScroll';
 
 import Icon from './icon';
 import Link from './link';
+import MediaPost from './media-post';
 import NavMenu from './nav-menu';
 import Status from './status';
 
@@ -33,12 +36,14 @@ function Timeline({
   boostsCarousel,
   fetchItems = () => {},
   checkForUpdates = () => {},
-  checkForUpdatesInterval = 60_000, // 1 minute
+  checkForUpdatesInterval = 15_000, // 15 seconds
   headerStart,
   headerEnd,
   timelineStart,
-  allowFilters,
+  // allowFilters,
   refresh,
+  view,
+  filterContext,
 }) {
   const snapStates = useSnapshot(states);
   const [items, setItems] = useState([]);
@@ -50,6 +55,7 @@ function Timeline({
 
   console.debug('RENDER Timeline', id, refresh);
 
+  const allowGrouping = view !== 'media';
   const loadItems = useDebouncedCallback(
     (firstLoad) => {
       setShowNew(false);
@@ -59,10 +65,12 @@ function Timeline({
         try {
           let { done, value } = await fetchItems(firstLoad);
           if (Array.isArray(value)) {
-            if (boostsCarousel) {
-              value = groupBoosts(value);
+            if (allowGrouping) {
+              if (boostsCarousel) {
+                value = groupBoosts(value);
+              }
+              value = groupContext(value);
             }
-            value = groupContext(value);
             console.log(value);
             if (firstLoad) {
               setItems(value);
@@ -210,25 +218,38 @@ function Timeline({
     }
   }, [nearReachEnd, showMore]);
 
+  const prevView = useRef(view);
+  useEffect(() => {
+    if (prevView.current !== view) {
+      prevView.current = view;
+      setItems([]);
+    }
+  }, [view]);
+
   const loadOrCheckUpdates = useCallback(
     async ({ disableIdleCheck = false } = {}) => {
-      console.log('✨ Load or check updates', {
+      const noPointers = scrollableRef.current
+        ? getComputedStyle(scrollableRef.current).pointerEvents === 'none'
+        : false;
+      console.log('✨ Load or check updates', id, {
         autoRefresh: snapStates.settings.autoRefresh,
         scrollTop: scrollableRef.current.scrollTop,
         disableIdleCheck,
         idle: window.__IDLE__,
         inBackground: inBackground(),
+        noPointers,
       });
       if (
         snapStates.settings.autoRefresh &&
-        scrollableRef.current.scrollTop === 0 &&
+        scrollableRef.current.scrollTop < 16 &&
         (disableIdleCheck || window.__IDLE__) &&
-        !inBackground()
+        !inBackground() &&
+        !noPointers
       ) {
-        console.log('✨ Load updates', snapStates.settings.autoRefresh);
+        console.log('✨ Load updates', id, snapStates.settings.autoRefresh);
         loadItems(true);
       } else {
-        console.log('✨ Check updates', snapStates.settings.autoRefresh);
+        console.log('✨ Check updates', id, snapStates.settings.autoRefresh);
         const hasUpdate = await checkForUpdates();
         if (hasUpdate) {
           console.log('✨ Has new updates', id);
@@ -238,10 +259,6 @@ function Timeline({
     },
     [id, loadItems, checkForUpdates, snapStates.settings.autoRefresh],
   );
-  const debouncedLoadOrCheckUpdates = useDebouncedCallback(
-    loadOrCheckUpdates,
-    3000,
-  );
 
   const lastHiddenTime = useRef();
   usePageVisibility(
@@ -249,14 +266,12 @@ function Timeline({
       if (visible) {
         const timeDiff = Date.now() - lastHiddenTime.current;
         if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
-          // 1 minute
-          debouncedLoadOrCheckUpdates({
+          loadOrCheckUpdates({
             disableIdleCheck: true,
           });
         }
       } else {
         lastHiddenTime.current = Date.now();
-        debouncedLoadOrCheckUpdates.cancel();
       }
       setVisible(visible);
     },
@@ -272,293 +287,334 @@ function Timeline({
   const hiddenUI = scrollDirection === 'end' && !nearReachStart;
 
   return (
-    <div
-      id={`${id}-page`}
-      class="deck-container"
-      ref={(node) => {
-        scrollableRef.current = node;
-        jRef.current = node;
-        kRef.current = node;
-        oRef.current = node;
-      }}
-      tabIndex="-1"
-    >
-      <div class="timeline-deck deck">
-        <header
-          hidden={hiddenUI}
-          onClick={(e) => {
-            if (!e.target.closest('a, button')) {
-              scrollableRef.current?.scrollTo({
-                top: 0,
-                behavior: 'smooth',
-              });
-            }
-          }}
-          onDblClick={(e) => {
-            if (!e.target.closest('a, button')) {
-              loadItems(true);
-            }
-          }}
-          class={uiState === 'loading' ? 'loading' : ''}
-        >
-          <div class="header-grid">
-            <div class="header-side">
-              <NavMenu />
-              {headerStart !== null && headerStart !== undefined ? (
-                headerStart
-              ) : (
-                <Link to="/" class="button plain home-button">
-                  <Icon icon="home" size="l" />
-                </Link>
-              )}
-            </div>
-            {title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
-            <div class="header-side">
-              {/* <Loader hidden={uiState !== 'loading'} /> */}
-              {!!headerEnd && headerEnd}
-            </div>
-          </div>
-          {items.length > 0 &&
-            uiState !== 'loading' &&
-            !hiddenUI &&
-            showNew && (
-              <button
-                class="updates-button shiny-pill"
-                type="button"
-                onClick={() => {
-                  loadItems(true);
-                  scrollableRef.current?.scrollTo({
-                    top: 0,
-                    behavior: 'smooth',
-                  });
-                }}
-              >
-                <Icon icon="arrow-up" /> New posts
-              </button>
-            )}
-        </header>
-        {!!timelineStart && (
-          <div
-            class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
+    <FilterContext.Provider value={filterContext}>
+      <div
+        id={`${id}-page`}
+        class="deck-container"
+        ref={(node) => {
+          scrollableRef.current = node;
+          jRef.current = node;
+          kRef.current = node;
+          oRef.current = node;
+        }}
+        tabIndex="-1"
+      >
+        <div class="timeline-deck deck">
+          <header
+            hidden={hiddenUI}
+            onClick={(e) => {
+              if (!e.target.closest('a, button')) {
+                scrollableRef.current?.scrollTo({
+                  top: 0,
+                  behavior: 'smooth',
+                });
+              }
+            }}
+            onDblClick={(e) => {
+              if (!e.target.closest('a, button')) {
+                loadItems(true);
+              }
+            }}
+            class={uiState === 'loading' ? 'loading' : ''}
           >
-            {timelineStart}
-          </div>
-        )}
-        {!!items.length ? (
-          <>
-            <ul class="timeline">
-              {items.map((status) => {
-                const { id: statusID, reblog, items, type, _pinned } = status;
-                const actualStatusID = reblog?.id || statusID;
-                const url = instance
-                  ? `/${instance}/s/${actualStatusID}`
-                  : `/s/${actualStatusID}`;
-                let title = '';
-                if (type === 'boosts') {
-                  title = `${items.length} Boosts`;
-                } else if (type === 'pinned') {
-                  title = 'Pinned posts';
-                }
-                const isCarousel = type === 'boosts' || type === 'pinned';
-                if (items) {
-                  if (isCarousel) {
-                    // Here, we don't hide filtered posts, but we sort them last
-                    items.sort((a, b) => {
-                      if (a._filtered && !b._filtered) {
-                        return 1;
-                      }
-                      if (!a._filtered && b._filtered) {
-                        return -1;
-                      }
-                      return 0;
+            <div class="header-grid">
+              <div class="header-side">
+                <NavMenu />
+                {headerStart !== null && headerStart !== undefined ? (
+                  headerStart
+                ) : (
+                  <Link to="/" class="button plain home-button">
+                    <Icon icon="home" size="l" />
+                  </Link>
+                )}
+              </div>
+              {title && (titleComponent ? titleComponent : <h1>{title}</h1>)}
+              <div class="header-side">
+                {/* <Loader hidden={uiState !== 'loading'} /> */}
+                {!!headerEnd && headerEnd}
+              </div>
+            </div>
+            {items.length > 0 &&
+              uiState !== 'loading' &&
+              !hiddenUI &&
+              showNew && (
+                <button
+                  class="updates-button shiny-pill"
+                  type="button"
+                  onClick={() => {
+                    loadItems(true);
+                    scrollableRef.current?.scrollTo({
+                      top: 0,
+                      behavior: 'smooth',
                     });
-                    return (
-                      <li
-                        key={`timeline-${statusID}`}
-                        class="timeline-item-carousel"
-                      >
-                        <StatusCarousel
-                          title={title}
-                          class={`${type}-carousel`}
-                        >
-                          {items.map((item) => {
-                            const { id: statusID, reblog } = item;
-                            const actualStatusID = reblog?.id || statusID;
-                            const url = instance
-                              ? `/${instance}/s/${actualStatusID}`
-                              : `/s/${actualStatusID}`;
-                            return (
-                              <li key={statusID}>
-                                <Link
-                                  class="status-carousel-link timeline-item-alt"
-                                  to={url}
-                                >
-                                  {useItemID ? (
-                                    <Status
-                                      statusID={statusID}
-                                      instance={instance}
-                                      size="s"
-                                      contentTextWeight
-                                      allowFilters={allowFilters}
-                                    />
-                                  ) : (
-                                    <Status
-                                      status={item}
-                                      instance={instance}
-                                      size="s"
-                                      contentTextWeight
-                                      allowFilters={allowFilters}
-                                    />
-                                  )}
-                                </Link>
-                              </li>
-                            );
-                          })}
-                        </StatusCarousel>
-                      </li>
-                    );
-                  }
-                  const manyItems = items.length > 3;
-                  return items.map((item, i) => {
-                    const { id: statusID, _differentAuthor } = item;
-                    const url = instance
-                      ? `/${instance}/s/${statusID}`
-                      : `/s/${statusID}`;
-                    const isMiddle = i > 0 && i < items.length - 1;
-                    const isSpoiler = item.sensitive && !!item.spoilerText;
-                    const showCompact =
-                      (!_differentAuthor && isSpoiler && i > 0) ||
-                      (manyItems &&
-                        isMiddle &&
-                        (type === 'thread' ||
-                          (type === 'conversation' &&
-                            !_differentAuthor &&
-                            !items[i - 1]._differentAuthor &&
-                            !items[i + 1]._differentAuthor)));
-                    return (
-                      <li
-                        key={`timeline-${statusID}`}
-                        class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
-                          i === 0
-                            ? 'start'
-                            : i === items.length - 1
-                            ? 'end'
-                            : 'middle'
-                        } ${
-                          _differentAuthor ? 'timeline-item-diff-author' : ''
-                        }`}
-                      >
-                        <Link class="status-link timeline-item" to={url}>
-                          {showCompact ? (
-                            <TimelineStatusCompact
-                              status={item}
-                              instance={instance}
-                            />
-                          ) : useItemID ? (
-                            <Status
-                              statusID={statusID}
-                              instance={instance}
-                              allowFilters={allowFilters}
-                            />
-                          ) : (
-                            <Status
-                              status={item}
-                              instance={instance}
-                              allowFilters={allowFilters}
-                            />
-                          )}
-                        </Link>
-                      </li>
-                    );
-                  });
-                }
-                return (
-                  <li key={`timeline-${statusID + _pinned}`}>
-                    <Link class="status-link timeline-item" to={url}>
-                      {useItemID ? (
-                        <Status
-                          statusID={statusID}
-                          instance={instance}
-                          allowFilters={allowFilters}
-                        />
-                      ) : (
-                        <Status
-                          status={status}
-                          instance={instance}
-                          allowFilters={allowFilters}
-                        />
-                      )}
-                    </Link>
-                  </li>
-                );
-              })}
-              {showMore && uiState === 'loading' && (
-                <>
-                  <li
-                    style={{
-                      height: '20vh',
-                    }}
-                  >
-                    <Status skeleton />
-                  </li>
-                  <li
-                    style={{
-                      height: '25vh',
-                    }}
-                  >
-                    <Status skeleton />
-                  </li>
-                </>
-              )}
-            </ul>
-            {uiState === 'default' &&
-              (showMore ? (
-                <InView
-                  onChange={(inView) => {
-                    if (inView) {
-                      loadItems();
-                    }
                   }}
                 >
-                  <button
-                    type="button"
-                    class="plain block"
-                    onClick={() => loadItems()}
-                    style={{ marginBlockEnd: '6em' }}
-                  >
-                    Show more&hellip;
-                  </button>
-                </InView>
-              ) : (
-                <p class="ui-state insignificant">The end.</p>
-              ))}
-          </>
-        ) : uiState === 'loading' ? (
-          <ul class="timeline">
-            {Array.from({ length: 5 }).map((_, i) => (
-              <li key={i}>
-                <Status skeleton />
-              </li>
-            ))}
-          </ul>
-        ) : (
-          uiState !== 'error' && <p class="ui-state">{emptyText}</p>
-        )}
-        {uiState === 'error' && (
-          <p class="ui-state">
-            {errorText}
-            <br />
-            <br />
-            <button
-              class="button plain"
-              onClick={() => loadItems(!items.length)}
+                  <Icon icon="arrow-up" /> New posts
+                </button>
+              )}
+          </header>
+          {!!timelineStart && (
+            <div
+              class={`timeline-start ${uiState === 'loading' ? 'loading' : ''}`}
             >
-              Try again
-            </button>
-          </p>
-        )}
+              {timelineStart}
+            </div>
+          )}
+          {!!items.length ? (
+            <>
+              <ul class={`timeline ${view ? `timeline-${view}` : ''}`}>
+                {items.map((status) => (
+                  <TimelineItem
+                    status={status}
+                    instance={instance}
+                    useItemID={useItemID}
+                    // allowFilters={allowFilters}
+                    filterContext={filterContext}
+                    key={status.id + status?._pinned}
+                    view={view}
+                  />
+                ))}
+                {showMore &&
+                  uiState === 'loading' &&
+                  (view === 'media' ? null : (
+                    <>
+                      <li
+                        style={{
+                          height: '20vh',
+                        }}
+                      >
+                        <Status skeleton />
+                      </li>
+                      <li
+                        style={{
+                          height: '25vh',
+                        }}
+                      >
+                        <Status skeleton />
+                      </li>
+                    </>
+                  ))}
+              </ul>
+              {uiState === 'default' &&
+                (showMore ? (
+                  <InView
+                    onChange={(inView) => {
+                      if (inView) {
+                        loadItems();
+                      }
+                    }}
+                  >
+                    <button
+                      type="button"
+                      class="plain block"
+                      onClick={() => loadItems()}
+                      style={{ marginBlockEnd: '6em' }}
+                    >
+                      Show more&hellip;
+                    </button>
+                  </InView>
+                ) : (
+                  <p class="ui-state insignificant">The end.</p>
+                ))}
+            </>
+          ) : uiState === 'loading' ? (
+            <ul class="timeline">
+              {Array.from({ length: 5 }).map((_, i) =>
+                view === 'media' ? (
+                  <div
+                    style={{
+                      height: '50vh',
+                    }}
+                  />
+                ) : (
+                  <li key={i}>
+                    <Status skeleton />
+                  </li>
+                ),
+              )}
+            </ul>
+          ) : (
+            uiState !== 'error' && <p class="ui-state">{emptyText}</p>
+          )}
+          {uiState === 'error' && (
+            <p class="ui-state">
+              {errorText}
+              <br />
+              <br />
+              <button type="button" onClick={() => loadItems(!items.length)}>
+                Try again
+              </button>
+            </p>
+          )}
+        </div>
       </div>
-    </div>
+    </FilterContext.Provider>
+  );
+}
+
+function TimelineItem({
+  status,
+  instance,
+  useItemID,
+  // allowFilters,
+  filterContext,
+  view,
+}) {
+  const { id: statusID, reblog, items, type, _pinned } = status;
+  const actualStatusID = reblog?.id || statusID;
+  const url = instance
+    ? `/${instance}/s/${actualStatusID}`
+    : `/s/${actualStatusID}`;
+  let title = '';
+  if (type === 'boosts') {
+    title = `${items.length} Boosts`;
+  } else if (type === 'pinned') {
+    title = 'Pinned posts';
+  }
+  const isCarousel = type === 'boosts' || type === 'pinned';
+  if (items) {
+    if (isCarousel) {
+      // Here, we don't hide filtered posts, but we sort them last
+      items.sort((a, b) => {
+        // if (a._filtered && !b._filtered) {
+        //   return 1;
+        // }
+        // if (!a._filtered && b._filtered) {
+        //   return -1;
+        // }
+        const aFiltered = isFiltered(a.filtered, filterContext);
+        const bFiltered = isFiltered(b.filtered, filterContext);
+        if (aFiltered && !bFiltered) {
+          return 1;
+        }
+        if (!aFiltered && bFiltered) {
+          return -1;
+        }
+        return 0;
+      });
+      return (
+        <li key={`timeline-${statusID}`} class="timeline-item-carousel">
+          <StatusCarousel title={title} class={`${type}-carousel`}>
+            {items.map((item) => {
+              const { id: statusID, reblog } = item;
+              const actualStatusID = reblog?.id || statusID;
+              const url = instance
+                ? `/${instance}/s/${actualStatusID}`
+                : `/s/${actualStatusID}`;
+              return (
+                <li key={statusID}>
+                  <Link class="status-carousel-link timeline-item-alt" to={url}>
+                    {useItemID ? (
+                      <Status
+                        statusID={statusID}
+                        instance={instance}
+                        size="s"
+                        contentTextWeight
+                        // allowFilters={allowFilters}
+                      />
+                    ) : (
+                      <Status
+                        status={item}
+                        instance={instance}
+                        size="s"
+                        contentTextWeight
+                        // allowFilters={allowFilters}
+                      />
+                    )}
+                  </Link>
+                </li>
+              );
+            })}
+          </StatusCarousel>
+        </li>
+      );
+    }
+    const manyItems = items.length > 3;
+    return items.map((item, i) => {
+      const { id: statusID, _differentAuthor } = item;
+      const url = instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`;
+      const isMiddle = i > 0 && i < items.length - 1;
+      const isSpoiler = item.sensitive && !!item.spoilerText;
+      const showCompact =
+        (!_differentAuthor && isSpoiler && i > 0) ||
+        (manyItems &&
+          isMiddle &&
+          (type === 'thread' ||
+            (type === 'conversation' &&
+              !_differentAuthor &&
+              !items[i - 1]._differentAuthor &&
+              !items[i + 1]._differentAuthor)));
+      return (
+        <li
+          key={`timeline-${statusID}`}
+          class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
+            i === 0 ? 'start' : i === items.length - 1 ? 'end' : 'middle'
+          } ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
+        >
+          <Link class="status-link timeline-item" to={url}>
+            {showCompact ? (
+              <TimelineStatusCompact status={item} instance={instance} />
+            ) : useItemID ? (
+              <Status
+                statusID={statusID}
+                instance={instance}
+                // allowFilters={allowFilters}
+              />
+            ) : (
+              <Status
+                status={item}
+                instance={instance}
+                // allowFilters={allowFilters}
+              />
+            )}
+          </Link>
+        </li>
+      );
+    });
+  }
+
+  const itemKey = `timeline-${statusID + _pinned}`;
+
+  if (view === 'media') {
+    return useItemID ? (
+      <MediaPost
+        class="timeline-item"
+        parent="li"
+        key={itemKey}
+        statusID={statusID}
+        instance={instance}
+        // allowFilters={allowFilters}
+      />
+    ) : (
+      <MediaPost
+        class="timeline-item"
+        parent="li"
+        key={itemKey}
+        status={status}
+        instance={instance}
+        // allowFilters={allowFilters}
+      />
+    );
+  }
+
+  return (
+    <li key={itemKey}>
+      <Link class="status-link timeline-item" to={url}>
+        {useItemID ? (
+          <Status
+            statusID={statusID}
+            instance={instance}
+            // allowFilters={allowFilters}
+          />
+        ) : (
+          <Status
+            status={status}
+            instance={instance}
+            // allowFilters={allowFilters}
+          />
+        )}
+      </Link>
+    </li>
   );
 }
 
diff --git a/src/components/translation-block.css b/src/components/translation-block.css
index 375a3afe..a1c83b9c 100644
--- a/src/components/translation-block.css
+++ b/src/components/translation-block.css
@@ -83,6 +83,7 @@
 .status-translation-block .translated-block output {
   display: block;
   margin-top: 0.75em;
+  text-wrap: pretty;
 }
 .status-translation-block
   .translated-block
diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx
index 3cd1c0a6..f7c61465 100644
--- a/src/pages/account-statuses.jsx
+++ b/src/pages/account-statuses.jsx
@@ -1,5 +1,11 @@
 import { MenuItem } from '@szhsin/react-menu';
-import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
+import {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'preact/hooks';
 import { useParams, useSearchParams } from 'react-router-dom';
 import { useSnapshot } from 'valtio';
 
@@ -51,6 +57,7 @@ function AccountStatuses() {
   const tagged = searchParams.get('tagged');
   const media = !!searchParams.get('media');
   const { masto, instance, authenticated } = api({ instance: params.instance });
+  const { masto: currentMasto, instance: currentInstance } = api();
   const accountStatusesIterator = useRef();
 
   const allSearchParams = [month, excludeReplies, excludeBoosts, tagged, media];
@@ -61,8 +68,8 @@ function AccountStatuses() {
   }, allSearchParams);
 
   const sameCurrentInstance = useMemo(
-    () => instance === api().instance,
-    [instance],
+    () => instance === currentInstance,
+    [instance, currentInstance],
   );
   const [searchEnabled, setSearchEnabled] = useState(false);
   useEffect(() => {
@@ -153,8 +160,8 @@ function AccountStatuses() {
         .next();
       if (pinnedStatuses?.length && !tagged && !media) {
         pinnedStatuses.forEach((status) => {
-          status._pinned = true;
           saveStatus(status, instance);
+          status._pinned = true;
         });
         if (pinnedStatuses.length >= 3) {
           const pinnedStatusesIds = pinnedStatuses.map((status) => status.id);
@@ -195,15 +202,41 @@ function AccountStatuses() {
 
   const [featuredTags, setFeaturedTags] = useState([]);
   useTitle(
-    `${account?.displayName ? account.displayName + ' ' : ''}@${
-      account?.acct ? account.acct : 'Account posts'
-    }`,
+    account?.acct
+      ? `${account?.displayName ? account.displayName + ' ' : ''}@${
+          account.acct
+        }${
+          !excludeReplies
+            ? ' (+ Replies)'
+            : excludeBoosts
+            ? ' (- Boosts)'
+            : tagged
+            ? ` (#${tagged})`
+            : media
+            ? ' (Media)'
+            : month
+            ? ` (${new Date(month).toLocaleString('default', {
+                month: 'long',
+                year: 'numeric',
+              })})`
+            : ''
+        }`
+      : 'Account posts',
     '/:instance?/a/:id',
   );
+
+  const fetchAccountPromiseRef = useRef();
+  const fetchAccount = useCallback(() => {
+    const fetchPromise =
+      fetchAccountPromiseRef.current || masto.v1.accounts.$select(id).fetch();
+    fetchAccountPromiseRef.current = fetchPromise;
+    return fetchPromise;
+  }, [id, masto]);
+
   useEffect(() => {
     (async () => {
       try {
-        const acc = await masto.v1.accounts.$select(id).fetch();
+        const acc = await fetchAccount();
         console.log(acc);
         setAccount(acc);
       } catch (e) {
@@ -223,21 +256,34 @@ function AccountStatuses() {
 
   const { displayName, acct, emojis } = account || {};
 
+  const accountInfoMemo = useMemo(() => {
+    const cachedAccount = snapStates.accounts[`${id}@${instance}`];
+    return (
+      <AccountInfo
+        instance={instance}
+        account={cachedAccount || id}
+        fetchAccount={fetchAccount}
+        authenticated={authenticated}
+        standalone
+      />
+    );
+  }, [id, instance, authenticated, fetchAccount]);
+
   const filterBarRef = useRef();
   const TimelineStart = useMemo(() => {
-    const cachedAccount = snapStates.accounts[`${id}@${instance}`];
     const filtered =
       !excludeReplies || excludeBoosts || tagged || media || !!month;
+
     return (
       <>
-        <AccountInfo
-          instance={instance}
-          account={cachedAccount || id}
-          fetchAccount={() => masto.v1.accounts.$select(id).fetch()}
-          authenticated={authenticated}
-          standalone
-        />
-        <div class="filter-bar" ref={filterBarRef}>
+        {accountInfoMemo}
+        <div
+          class="filter-bar"
+          ref={filterBarRef}
+          style={{
+            position: 'relative',
+          }}
+        >
           {filtered ? (
             <Link
               to={`/${instance}/a/${id}`}
@@ -328,6 +374,15 @@ function AccountStatuses() {
                           }
                         : {},
                     );
+                    showToast(
+                      `Showing posts in ${new Date(value).toLocaleString(
+                        'default',
+                        {
+                          month: 'long',
+                          year: 'numeric',
+                        },
+                      )}`,
+                    );
                   }}
                 />
               </label>
@@ -392,7 +447,7 @@ function AccountStatuses() {
       title={`${account?.acct ? '@' + account.acct : 'Posts'}`}
       titleComponent={
         <h1
-          class="header-account"
+          class="header-double-lines header-account"
           // onClick={() => {
           //   states.showAccount = {
           //     account,
@@ -414,6 +469,7 @@ function AccountStatuses() {
       errorText="Unable to load posts"
       fetchItems={fetchAccountStatuses}
       useItemID
+      view={media ? 'media' : undefined}
       boostsCarousel={snapStates.settings.boostsCarousel}
       timelineStart={TimelineStart}
       refresh={[
@@ -461,6 +517,29 @@ function AccountStatuses() {
               Switch to account's instance (<b>{accountInstance}</b>)
             </small>
           </MenuItem>
+          {!sameCurrentInstance && (
+            <MenuItem
+              onClick={() => {
+                (async () => {
+                  try {
+                    const acc = await currentMasto.v1.accounts.lookup({
+                      acct: account.acct + '@' + instance,
+                    });
+                    const { id } = acc;
+                    location.hash = `/${currentInstance}/a/${id}`;
+                  } catch (e) {
+                    console.error(e);
+                    alert('Unable to fetch account info');
+                  }
+                })();
+              }}
+            >
+              <Icon icon="transfer" />{' '}
+              <small class="menu-double-lines">
+                Switch to my instance (<b>{currentInstance}</b>)
+              </small>
+            </MenuItem>
+          )}
         </Menu2>
       }
     />
diff --git a/src/pages/following.jsx b/src/pages/following.jsx
index 3d081f32..c0505fec 100644
--- a/src/pages/following.jsx
+++ b/src/pages/following.jsx
@@ -32,7 +32,7 @@ function Following({ title, path, id, ...props }) {
         console.log('First load', latestItem.current);
       }
 
-      value = filteredItems(value, 'home');
+      // value = filteredItems(value, 'home');
       value.forEach((item) => {
         saveStatus(item, instance);
       });
@@ -115,7 +115,8 @@ function Following({ title, path, id, ...props }) {
       useItemID
       boostsCarousel={snapStates.settings.boostsCarousel}
       {...props}
-      allowFilters
+      // allowFilters
+      filterContext="home"
     />
   );
 }
diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx
index 1efb6aa2..4f5bfbdc 100644
--- a/src/pages/hashtag.jsx
+++ b/src/pages/hashtag.jsx
@@ -2,16 +2,19 @@ import {
   FocusableItem,
   MenuDivider,
   MenuGroup,
+  MenuHeader,
   MenuItem,
 } from '@szhsin/react-menu';
 import { useEffect, useRef, useState } from 'preact/hooks';
-import { useNavigate, useParams } from 'react-router-dom';
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
 
 import Icon from '../components/icon';
 import Menu2 from '../components/menu2';
 import MenuConfirm from '../components/menu-confirm';
+import { SHORTCUTS_LIMIT } from '../components/shortcuts-settings';
 import Timeline from '../components/timeline';
 import { api } from '../utils/api';
+import { filteredItems } from '../utils/filters';
 import showToast from '../utils/show-toast';
 import states from '../utils/states';
 import { saveStatus } from '../utils/states';
@@ -25,20 +28,30 @@ const LIMIT = 20;
 const TAGS_LIMIT_PER_MODE = 4;
 const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1;
 
-function Hashtags({ columnMode, ...props }) {
+function Hashtags({ media: mediaView, columnMode, ...props }) {
   // const navigate = useNavigate();
   let { hashtag, ...params } = columnMode ? {} : useParams();
   if (props.hashtag) hashtag = props.hashtag;
   let hashtags = hashtag.trim().split(/[\s+]+/);
   hashtags.sort();
   hashtag = hashtags[0];
+  const [searchParams, setSearchParams] = useSearchParams();
+  const media = mediaView || !!searchParams.get('media');
+  const linkParams = media ? '?media=1' : '';
 
   const { masto, instance, authenticated } = api({
     instance: props?.instance || params.instance,
   });
-  const { authenticated: currentAuthenticated } = api();
+  const {
+    masto: currentMasto,
+    instance: currentInstance,
+    authenticated: currentAuthenticated,
+  } = api();
   const hashtagTitle = hashtags.map((t) => `#${t}`).join(' ');
-  const title = instance ? `${hashtagTitle} on ${instance}` : hashtagTitle;
+  const hashtagPostTitle = media ? ` (Media only)` : '';
+  const title = instance
+    ? `${hashtagTitle}${hashtagPostTitle} on ${instance}`
+    : `${hashtagTitle}${hashtagPostTitle}`;
   useTitle(title, `/:instance?/t/:hashtag`);
   const latestItem = useRef();
 
@@ -60,16 +73,20 @@ function Hashtags({ columnMode, ...props }) {
         limit: LIMIT,
         any: hashtags.slice(1),
         maxId: firstLoad ? undefined : maxID.current,
+        onlyMedia: media,
       })
       .next();
-    const { value } = results;
+    let { value } = results;
     if (value?.length) {
       if (firstLoad) {
         latestItem.current = value[0].id;
       }
 
+      // value = filteredItems(value, 'public');
       value.forEach((item) => {
-        saveStatus(item, instance);
+        saveStatus(item, instance, {
+          skipThreading: media, // If media view, no need to form threads
+        });
       });
 
       maxID.current = value[value.length - 1].id;
@@ -88,9 +105,11 @@ function Hashtags({ columnMode, ...props }) {
           limit: 1,
           any: hashtags.slice(1),
           since_id: latestItem.current,
+          onlyMedia: media,
         })
         .next();
-      const { value } = results;
+      let { value } = results;
+      value = filteredItems(value, 'public');
       if (value?.length) {
         return true;
       }
@@ -123,7 +142,7 @@ function Hashtags({ columnMode, ...props }) {
       title={title}
       titleComponent={
         !!instance && (
-          <h1 class="header-account">
+          <h1 class="header-double-lines">
             <b>{hashtagTitle}</b>
             <div>{instance}</div>
           </h1>
@@ -136,6 +155,10 @@ function Hashtags({ columnMode, ...props }) {
       fetchItems={fetchHashtags}
       checkForUpdates={checkForUpdates}
       useItemID
+      view={media ? 'media' : undefined}
+      refresh={media}
+      // allowFilters
+      filterContext="public"
       headerEnd={
         <Menu2
           portal
@@ -209,6 +232,23 @@ function Hashtags({ columnMode, ...props }) {
               <MenuDivider />
             </>
           )}
+          <MenuHeader className="plain">Filters</MenuHeader>
+          <MenuItem
+            type="checkbox"
+            checked={!!media}
+            onClick={() => {
+              if (media) {
+                searchParams.delete('media');
+              } else {
+                searchParams.set('media', '1');
+              }
+              setSearchParams(searchParams);
+            }}
+          >
+            <Icon icon="check-circle" />{' '}
+            <span class="menu-grow">Media only</span>
+          </MenuItem>
+          <MenuDivider />
           <FocusableItem className="menu-field" disabled={reachLimit}>
             {({ ref }) => (
               <form
@@ -231,7 +271,7 @@ function Hashtags({ columnMode, ...props }) {
                     // );
                     location.hash = instance
                       ? `/${instance}/t/${hashtags.join('+')}`
-                      : `/t/${hashtags.join('+')}`;
+                      : `/t/${hashtags.join('+')}${linkParams}`;
                   }
                 }}
               >
@@ -267,8 +307,8 @@ function Hashtags({ columnMode, ...props }) {
                   //     : `/t/${hashtags.join('+')}`,
                   // );
                   location.hash = instance
-                    ? `/${instance}/t/${hashtags.join('+')}`
-                    : `/t/${hashtags.join('+')}`;
+                    ? `/${instance}/t/${hashtags.join('+')}${linkParams}`
+                    : `/t/${hashtags.join('+')}${linkParams}`;
                 }}
               >
                 <Icon icon="x" alt="Remove hashtag" class="danger-icon" />
@@ -283,10 +323,17 @@ function Hashtags({ columnMode, ...props }) {
           <MenuItem
             disabled={!currentAuthenticated}
             onClick={() => {
+              if (states.shortcuts.length >= SHORTCUTS_LIMIT) {
+                alert(
+                  `Max ${SHORTCUTS_LIMIT} shortcuts reached. Unable to add shortcut.`,
+                );
+                return;
+              }
               const shortcut = {
                 type: 'hashtag',
                 hashtag: hashtags.join(' '),
                 instance,
+                media: media ? 'on' : undefined,
               };
               // Check if already exists
               const exists = states.shortcuts.some(
@@ -300,7 +347,8 @@ function Hashtags({ columnMode, ...props }) {
                       .split(/[\s+]+/)
                       .sort()
                       .join(' ') &&
-                  (s.instance ? s.instance === shortcut.instance : true),
+                  (s.instance ? s.instance === shortcut.instance : true) &&
+                  (s.media ? !!s.media === !!shortcut.media : true),
               );
               if (exists) {
                 alert('This shortcut already exists');
@@ -324,12 +372,28 @@ function Hashtags({ columnMode, ...props }) {
               if (newInstance) {
                 newInstance = newInstance.toLowerCase().trim();
                 // navigate(`/${newInstance}/t/${hashtags.join('+')}`);
-                location.hash = `/${newInstance}/t/${hashtags.join('+')}`;
+                location.hash = `/${newInstance}/t/${hashtags.join(
+                  '+',
+                )}${linkParams}`;
               }
             }}
           >
             <Icon icon="bus" /> <span>Go to another instance…</span>
           </MenuItem>
+          {currentInstance !== instance && (
+            <MenuItem
+              onClick={() => {
+                location.hash = `/${currentInstance}/t/${hashtags.join(
+                  '+',
+                )}${linkParams}`;
+              }}
+            >
+              <Icon icon="bus" />{' '}
+              <small class="menu-double-lines">
+                Go to my instance (<b>{currentInstance}</b>)
+              </small>
+            </MenuItem>
+          )}
         </Menu2>
       }
     />
diff --git a/src/pages/home.jsx b/src/pages/home.jsx
index 35176112..fe4503cd 100644
--- a/src/pages/home.jsx
+++ b/src/pages/home.jsx
@@ -35,8 +35,9 @@ function Home() {
 
   return (
     <>
-      {(snapStates.settings.shortcutsColumnsMode ||
-        snapStates.settings.shortcutsViewMode === 'multi-column') &&
+      {(snapStates.settings.shortcutsViewMode === 'multi-column' ||
+        (!snapStates.settings.shortcutsViewMode &&
+          snapStates.settings.shortcutsColumnsMode)) &&
       !!snapStates.shortcuts?.length ? (
         <Columns />
       ) : (
diff --git a/src/pages/list.jsx b/src/pages/list.jsx
index d6f9a93c..1bc6b845 100644
--- a/src/pages/list.jsx
+++ b/src/pages/list.jsx
@@ -43,7 +43,7 @@ function List(props) {
         latestItem.current = value[0].id;
       }
 
-      value = filteredItems(value, 'home');
+      // value = filteredItems(value, 'home');
       value.forEach((item) => {
         saveStatus(item, instance);
       });
@@ -102,7 +102,8 @@ function List(props) {
         checkForUpdates={checkForUpdates}
         useItemID
         boostsCarousel={snapStates.settings.boostsCarousel}
-        allowFilters
+        // allowFilters
+        filterContext="home"
         // refresh={reloadCount}
         headerStart={
           <Link to="/l" class="button plain">
diff --git a/src/pages/mentions.jsx b/src/pages/mentions.jsx
index 6403d188..da6e5ae0 100644
--- a/src/pages/mentions.jsx
+++ b/src/pages/mentions.jsx
@@ -11,11 +11,11 @@ const LIMIT = 20;
 const emptySearchParams = new URLSearchParams();
 
 function Mentions({ columnMode, ...props }) {
-  useTitle('Mentions', '/mentions');
   const { masto, instance } = api();
   const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams();
   const [stateType, setStateType] = useState(null);
   const type = props?.type || searchParams.get('type') || stateType;
+  useTitle(`Mentions${type === 'private' ? ' (Private)' : ''}`, '/mentions');
 
   const mentionsIterator = useRef();
   const latestItem = useRef();
diff --git a/src/pages/notifications.css b/src/pages/notifications.css
index 337812bc..a12becd3 100644
--- a/src/pages/notifications.css
+++ b/src/pages/notifications.css
@@ -162,9 +162,10 @@
   flex-grow: 1;
   min-width: 0;
 }
-.notification-content p:first-child {
+.notification-content > p:first-child {
   margin-top: 0;
   margin-bottom: 8px;
+  text-wrap: pretty;
 }
 
 .notification-group-statuses {
@@ -407,6 +408,7 @@
   margin-block: min(0.75em, 12px);
   white-space: pre-wrap;
   tab-size: 2;
+  text-wrap: pretty;
 }
 .announcements .announcement-reactions:not(:hidden) {
   display: flex;
diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx
index 824e7b7d..6c48ec2a 100644
--- a/src/pages/notifications.jsx
+++ b/src/pages/notifications.jsx
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
 import { InView } from 'react-intersection-observer';
 import { useSearchParams } from 'react-router-dom';
 import { useSnapshot } from 'valtio';
+import { subscribeKey } from 'valtio/utils';
 
 import AccountBlock from '../components/account-block';
 import FollowRequestButtons from '../components/follow-request-buttons';
@@ -23,6 +24,7 @@ import { getRegistration } from '../utils/push-notifications';
 import shortenNumber from '../utils/shorten-number';
 import states, { saveStatus } from '../utils/states';
 import { getCurrentInstance } from '../utils/store-utils';
+import usePageVisibility from '../utils/usePageVisibility';
 import useScroll from '../utils/useScroll';
 import useTitle from '../utils/useTitle';
 
@@ -132,20 +134,25 @@ function Notifications({ columnMode }) {
     (async () => {
       try {
         const fetchNotificationsPromise = fetchNotifications(firstLoad);
-        const fetchFollowRequestsPromise = fetchFollowRequests();
-        const fetchAnnouncementsPromise = fetchAnnouncements();
 
         if (firstLoad) {
-          const announcements = await fetchAnnouncementsPromise;
-          announcements.sort((a, b) => {
-            // Sort by updatedAt first, then createdAt
-            const aDate = new Date(a.updatedAt || a.createdAt);
-            const bDate = new Date(b.updatedAt || b.createdAt);
-            return bDate - aDate;
-          });
-          setAnnouncements(announcements);
-          const requests = await fetchFollowRequestsPromise;
-          setFollowRequests(requests);
+          fetchAnnouncements()
+            .then((announcements) => {
+              announcements.sort((a, b) => {
+                // Sort by updatedAt first, then createdAt
+                const aDate = new Date(a.updatedAt || a.createdAt);
+                const bDate = new Date(b.updatedAt || b.createdAt);
+                return bDate - aDate;
+              });
+              setAnnouncements(announcements);
+            })
+            .catch(() => {});
+
+          fetchFollowRequests()
+            .then((requests) => {
+              setFollowRequests(requests);
+            })
+            .catch(() => {});
         }
 
         const { done } = await fetchNotificationsPromise;
@@ -173,30 +180,59 @@ function Notifications({ columnMode }) {
   //   }
   // }, [nearReachEnd, showMore]);
 
-  const loadUpdates = useCallback(() => {
-    console.log('✨ Load updates', {
-      autoRefresh: snapStates.settings.autoRefresh,
-      scrollTop: scrollableRef.current?.scrollTop === 0,
-      inBackground: inBackground(),
-      notificationsShowNew: snapStates.notificationsShowNew,
-      uiState,
-    });
-    if (
-      snapStates.settings.autoRefresh &&
-      scrollableRef.current?.scrollTop === 0 &&
-      window.__IDLE__ &&
-      !inBackground() &&
-      snapStates.notificationsShowNew &&
-      uiState !== 'loading'
-    ) {
-      loadNotifications(true);
+  const [showNew, setShowNew] = useState(false);
+
+  const loadUpdates = useCallback(
+    ({ disableIdleCheck = false } = {}) => {
+      if (uiState === 'loading') {
+        return;
+      }
+      console.log('✨ Load updates', {
+        autoRefresh: snapStates.settings.autoRefresh,
+        scrollTop: scrollableRef.current?.scrollTop,
+        inBackground: inBackground(),
+        disableIdleCheck,
+        notificationsShowNew: snapStates.notificationsShowNew,
+      });
+      if (
+        snapStates.settings.autoRefresh &&
+        scrollableRef.current?.scrollTop < 16 &&
+        (disableIdleCheck || window.__IDLE__) &&
+        !inBackground() &&
+        snapStates.notificationsShowNew
+      ) {
+        setShowNew(false);
+        loadNotifications(true);
+      } else {
+        setShowNew(snapStates.notificationsShowNew);
+      }
+    },
+    [snapStates.notificationsShowNew, snapStates.settings.autoRefresh, uiState],
+  );
+  // useEffect(loadUpdates, [snapStates.notificationsShowNew]);
+
+  const lastHiddenTime = useRef();
+  usePageVisibility((visible) => {
+    let unsub;
+    if (visible) {
+      const timeDiff = Date.now() - lastHiddenTime.current;
+      if (!lastHiddenTime.current || timeDiff > 1000 * 60) {
+        loadUpdates({
+          disableIdleCheck: true,
+        });
+      } else {
+        lastHiddenTime.current = Date.now();
+      }
+      unsub = subscribeKey(states, 'notificationsShowNew', (v) => {
+        if (v) {
+          loadUpdates();
+        }
+      });
     }
-  }, [
-    snapStates.notificationsShowNew,
-    snapStates.settings.autoRefresh,
-    uiState,
-  ]);
-  useEffect(loadUpdates, [snapStates.notificationsShowNew]);
+    return () => {
+      unsub?.();
+    };
+  });
 
   const todayDate = new Date();
   const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
@@ -265,7 +301,7 @@ function Notifications({ columnMode }) {
               {/* <Loader hidden={uiState !== 'loading'} /> */}
             </div>
           </div>
-          {snapStates.notificationsShowNew && uiState !== 'loading' && (
+          {showNew && uiState !== 'loading' && (
             <button
               class="updates-button shiny-pill"
               type="button"
diff --git a/src/pages/public.jsx b/src/pages/public.jsx
index 2fac5b3f..089d084a 100644
--- a/src/pages/public.jsx
+++ b/src/pages/public.jsx
@@ -21,6 +21,7 @@ function Public({ local, columnMode, ...props }) {
   const { masto, instance } = api({
     instance: props?.instance || params.instance,
   });
+  const { masto: currentMasto, instance: currentInstance } = api();
   const title = `${isLocal ? 'Local' : 'Federated'} timeline (${instance})`;
   useTitle(title, isLocal ? `/:instance?/p/l` : `/:instance?/p`);
   // const navigate = useNavigate();
@@ -41,7 +42,7 @@ function Public({ local, columnMode, ...props }) {
         latestItem.current = value[0].id;
       }
 
-      value = filteredItems(value, 'public');
+      // value = filteredItems(value, 'public');
       value.forEach((item) => {
         saveStatus(item, instance);
       });
@@ -77,7 +78,7 @@ function Public({ local, columnMode, ...props }) {
       key={instance + isLocal}
       title={title}
       titleComponent={
-        <h1 class="header-account">
+        <h1 class="header-double-lines">
           <b>{isLocal ? 'Local timeline' : 'Federated timeline'}</b>
           <div>{instance}</div>
         </h1>
@@ -91,7 +92,8 @@ function Public({ local, columnMode, ...props }) {
       useItemID
       headerStart={<></>}
       boostsCarousel={snapStates.settings.boostsCarousel}
-      allowFilters
+      // allowFilters
+      filterContext="public"
       headerEnd={
         <Menu2
           portal
@@ -137,6 +139,20 @@ function Public({ local, columnMode, ...props }) {
           >
             <Icon icon="bus" /> <span>Go to another instance…</span>
           </MenuItem>
+          {currentInstance !== instance && (
+            <MenuItem
+              onClick={() => {
+                location.hash = isLocal
+                  ? `/${currentInstance}/p/l`
+                  : `/${currentInstance}/p`;
+              }}
+            >
+              <Icon icon="bus" />{' '}
+              <small class="menu-double-lines">
+                Go to my instance (<b>{currentInstance}</b>)
+              </small>
+            </MenuItem>
+          )}
         </Menu2>
       }
     />
diff --git a/src/pages/search.jsx b/src/pages/search.jsx
index 25a9739a..7c4587cb 100644
--- a/src/pages/search.jsx
+++ b/src/pages/search.jsx
@@ -1,5 +1,6 @@
 import './search.css';
 
+import { useAutoAnimate } from '@formkit/auto-animate/preact';
 import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
 import { useHotkeys } from 'react-hotkeys-hook';
 import { InView } from 'react-intersection-observer';
@@ -13,6 +14,7 @@ import NavMenu from '../components/nav-menu';
 import SearchForm from '../components/search-form';
 import Status from '../components/status';
 import { api } from '../utils/api';
+import shortenNumber from '../utils/shorten-number';
 import useTitle from '../utils/useTitle';
 
 const SHORT_LIMIT = 5;
@@ -144,6 +146,8 @@ function Search(props) {
     },
   );
 
+  const [filterBarParent] = useAutoAnimate();
+
   return (
     <div id="search-page" class="deck-container" ref={scrollableRef}>
       <div class="timeline-deck deck">
@@ -158,7 +162,10 @@ function Search(props) {
         </header>
         <main>
           {!!q && (
-            <div class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}>
+            <div
+              ref={filterBarParent}
+              class={`filter-bar ${uiState === 'loading' ? 'loading' : ''}`}
+            >
               {!!type && (
                 <Link to={`/search${q ? `?q=${encodeURIComponent(q)}` : ''}`}>
                   ‹ All
@@ -244,20 +251,32 @@ function Search(props) {
                   {hashtagResults.length > 0 ? (
                     <>
                       <ul class="link-list hashtag-list">
-                        {hashtagResults.map((hashtag) => (
-                          <li key={hashtag.name}>
-                            <Link
-                              to={
-                                instance
-                                  ? `/${instance}/t/${hashtag.name}`
-                                  : `/t/${hashtag.name}`
-                              }
-                            >
-                              <Icon icon="hashtag" />
-                              <span>{hashtag.name}</span>
-                            </Link>
-                          </li>
-                        ))}
+                        {hashtagResults.map((hashtag) => {
+                          const { name, history } = hashtag;
+                          const total = history.reduce(
+                            (acc, cur) => acc + +cur.uses,
+                            0,
+                          );
+                          return (
+                            <li key={hashtag.name}>
+                              <Link
+                                to={
+                                  instance
+                                    ? `/${instance}/t/${hashtag.name}`
+                                    : `/t/${hashtag.name}`
+                                }
+                              >
+                                <Icon icon="hashtag" />
+                                <span>{hashtag.name}</span>
+                                {!!total && (
+                                  <span class="count">
+                                    {shortenNumber(total)}
+                                  </span>
+                                )}
+                              </Link>
+                            </li>
+                          );
+                        })}
                       </ul>
                       {type !== 'hashtags' && (
                         <div class="ui-state">
diff --git a/src/pages/status.jsx b/src/pages/status.jsx
index 5c074126..e8de6597 100644
--- a/src/pages/status.jsx
+++ b/src/pages/status.jsx
@@ -60,6 +60,14 @@ const scrollIntoViewOptions = {
   behavior: 'smooth',
 };
 
+// Select all statuses except those inside collapsed details/summary
+// Hat-tip to @AmeliaBR@front-end.social
+// https://front-end.social/@AmeliaBR/109784776146144471
+const STATUSES_SELECTOR =
+  '.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)';
+
+const STATUS_URL_REGEX = /\/s\//i;
+
 function StatusPage(params) {
   const { id } = params;
   const { masto, instance } = api({ instance: params.instance });
@@ -167,8 +175,13 @@ function StatusPage(params) {
 function StatusThread({ id, closeLink = '/', instance: propInstance }) {
   const [searchParams, setSearchParams] = useSearchParams();
   const mediaParam = searchParams.get('media');
+  const mediaStatusID = searchParams.get('mediaStatusID');
   const showMedia = parseInt(mediaParam, 10) > 0;
-  const firstLoad = useRef(!states.prevLocation && history.length === 1);
+  const firstLoad = useRef(
+    !states.prevLocation &&
+      (history.length === 1 ||
+        ('navigation' in window && navigation?.entries?.()?.length === 1)),
+  );
   const [viewMode, setViewMode] = useState(
     searchParams.get('view') || firstLoad.current ? 'full' : null,
   );
@@ -554,12 +567,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
     );
     const activeStatusRect = activeStatus?.getBoundingClientRect();
     const allStatusLinks = Array.from(
-      // Select all statuses except those inside collapsed details/summary
-      // Hat-tip to @AmeliaBR@front-end.social
-      // https://front-end.social/@AmeliaBR/109784776146144471
-      scrollableRef.current.querySelectorAll(
-        '.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
-      ),
+      scrollableRef.current.querySelectorAll(STATUSES_SELECTOR),
     );
     console.log({ allStatusLinks });
     if (
@@ -592,9 +600,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
     );
     const activeStatusRect = activeStatus?.getBoundingClientRect();
     const allStatusLinks = Array.from(
-      scrollableRef.current.querySelectorAll(
-        '.status-link:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *), .status-focus:not(details:not([open]) > summary ~ *, details:not([open]) > summary ~ * *)',
-      ),
+      scrollableRef.current.querySelectorAll(STATUSES_SELECTOR),
     );
     if (
       activeStatus &&
@@ -657,139 +663,159 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
     resetScrollPosition(status.id);
   }, []);
 
-  const renderStatus = (status) => {
-    const {
-      id: statusID,
-      ancestor,
-      isThread,
-      descendant,
-      thread,
-      replies,
-      repliesCount,
-      weight,
-    } = status;
-    const isHero = statusID === id;
-    // const StatusParent = useCallback(
-    //   (props) =>
-    //     isThread || thread || ancestor ? (
-    //       <Link
-    //         class="status-link"
-    //         to={
-    //           instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
-    //         }
-    //         onClick={() => {
-    //           resetScrollPosition(statusID);
-    //         }}
-    //         {...props}
-    //       />
-    //     ) : (
-    //       <div class="status-focus" tabIndex={0} {...props} />
-    //     ),
-    //   [isThread, thread],
-    // );
-    return (
-      <li
-        key={statusID}
-        ref={isHero ? heroStatusRef : null}
-        class={`${ancestor ? 'ancestor' : ''} ${
-          descendant ? 'descendant' : ''
-        } ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
-      >
-        {isHero ? (
-          <>
-            <InView
-              threshold={0.1}
-              onChange={onView}
-              class="status-focus"
-              tabIndex={0}
+  useEffect(() => {
+    let timer;
+    if (mediaStatusID && showMedia) {
+      timer = setTimeout(() => {
+        const status = scrollableRef.current?.querySelector(
+          `.status-link[href*="/${mediaStatusID}"]`,
+        );
+        if (status) {
+          status.scrollIntoView(scrollIntoViewOptions);
+        }
+      }, 400); // After CSS transition
+    }
+    return () => {
+      clearTimeout(timer);
+    };
+  }, [mediaStatusID, showMedia]);
+
+  const renderStatus = useCallback(
+    (status) => {
+      const {
+        id: statusID,
+        ancestor,
+        isThread,
+        descendant,
+        thread,
+        replies,
+        repliesCount,
+        weight,
+      } = status;
+      const isHero = statusID === id;
+      // const StatusParent = useCallback(
+      //   (props) =>
+      //     isThread || thread || ancestor ? (
+      //       <Link
+      //         class="status-link"
+      //         to={
+      //           instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
+      //         }
+      //         onClick={() => {
+      //           resetScrollPosition(statusID);
+      //         }}
+      //         {...props}
+      //       />
+      //     ) : (
+      //       <div class="status-focus" tabIndex={0} {...props} />
+      //     ),
+      //   [isThread, thread],
+      // );
+      return (
+        <li
+          key={statusID}
+          ref={isHero ? heroStatusRef : null}
+          class={`${ancestor ? 'ancestor' : ''} ${
+            descendant ? 'descendant' : ''
+          } ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
+        >
+          {isHero ? (
+            <>
+              <InView
+                threshold={0.1}
+                onChange={onView}
+                class="status-focus"
+                tabIndex={0}
+              >
+                <Status
+                  statusID={statusID}
+                  instance={instance}
+                  withinContext
+                  size="l"
+                  enableTranslate
+                  forceTranslate={translate}
+                />
+              </InView>
+              {uiState !== 'loading' && !authenticated ? (
+                <div class="post-status-banner">
+                  <p>
+                    You're not logged in. Interactions (reply, boost, etc) are
+                    not possible.
+                  </p>
+                  <Link to="/login" class="button">
+                    Log in
+                  </Link>
+                </div>
+              ) : (
+                !sameInstance && (
+                  <div class="post-status-banner">
+                    <p>
+                      This post is from another instance (<b>{instance}</b>).
+                      Interactions (reply, boost, etc) are not possible.
+                    </p>
+                    <button
+                      type="button"
+                      disabled={uiState === 'loading'}
+                      onClick={() => {
+                        setUIState('loading');
+                        (async () => {
+                          try {
+                            const results = await currentMasto.v2.search.fetch({
+                              q: heroStatus.url,
+                              type: 'statuses',
+                              resolve: true,
+                              limit: 1,
+                            });
+                            if (results.statuses.length) {
+                              const status = results.statuses[0];
+                              location.hash = currentInstance
+                                ? `/${currentInstance}/s/${status.id}`
+                                : `/s/${status.id}`;
+                            } else {
+                              throw new Error('No results');
+                            }
+                          } catch (e) {
+                            setUIState('default');
+                            alert('Error: ' + e);
+                            console.error(e);
+                          }
+                        })();
+                      }}
+                    >
+                      <Icon icon="transfer" /> Switch to my instance to enable
+                      interactions
+                    </button>
+                  </div>
+                )
+              )}
+            </>
+          ) : (
+            // <StatusParent>
+            <Link
+              class="status-link"
+              to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
+              onClick={() => {
+                resetScrollPosition(statusID);
+              }}
             >
               <Status
                 statusID={statusID}
                 instance={instance}
                 withinContext
-                size="l"
+                size={thread || ancestor ? 'm' : 's'}
                 enableTranslate
-                forceTranslate={translate}
+                onMediaClick={handleMediaClick}
+                onStatusLinkClick={handleStatusLinkClick}
               />
-            </InView>
-            {uiState !== 'loading' && !authenticated ? (
-              <div class="post-status-banner">
-                <p>
-                  You're not logged in. Interactions (reply, boost, etc) are not
-                  possible.
-                </p>
-                <Link to="/login" class="button">
-                  Log in
-                </Link>
-              </div>
-            ) : (
-              !sameInstance && (
-                <div class="post-status-banner">
-                  <p>
-                    This post is from another instance (<b>{instance}</b>).
-                    Interactions (reply, boost, etc) are not possible.
-                  </p>
-                  <button
-                    type="button"
-                    disabled={uiState === 'loading'}
-                    onClick={() => {
-                      setUIState('loading');
-                      (async () => {
-                        try {
-                          const results = await currentMasto.v2.search.fetch({
-                            q: heroStatus.url,
-                            type: 'statuses',
-                            resolve: true,
-                            limit: 1,
-                          });
-                          if (results.statuses.length) {
-                            const status = results.statuses[0];
-                            location.hash = currentInstance
-                              ? `/${currentInstance}/s/${status.id}`
-                              : `/s/${status.id}`;
-                          } else {
-                            throw new Error('No results');
-                          }
-                        } catch (e) {
-                          setUIState('default');
-                          alert('Error: ' + e);
-                          console.error(e);
-                        }
-                      })();
-                    }}
-                  >
-                    <Icon icon="transfer" /> Switch to my instance to enable
-                    interactions
-                  </button>
+              {ancestor && isThread && repliesCount > 1 && (
+                <div class="replies-link">
+                  <Icon icon="comment" />{' '}
+                  <span title={repliesCount}>
+                    {shortenNumber(repliesCount)}
+                  </span>
                 </div>
-              )
-            )}
-          </>
-        ) : (
-          // <StatusParent>
-          <Link
-            class="status-link"
-            to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
-            onClick={() => {
-              resetScrollPosition(statusID);
-            }}
-          >
-            <Status
-              statusID={statusID}
-              instance={instance}
-              withinContext
-              size={thread || ancestor ? 'm' : 's'}
-              enableTranslate
-              onMediaClick={handleMediaClick}
-              onStatusLinkClick={handleStatusLinkClick}
-            />
-            {ancestor && isThread && repliesCount > 1 && (
-              <div class="replies-link">
-                <Icon icon="comment" />{' '}
-                <span title={repliesCount}>{shortenNumber(repliesCount)}</span>
-              </div>
-            )}{' '}
-            {/* {replies?.length > LIMIT && (
+              )}{' '}
+              {/* {replies?.length > LIMIT && (
                         <div class="replies-link">
                           <Icon icon="comment" />{' '}
                           <span title={replies.length}>
@@ -797,48 +823,71 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
                           </span>
                         </div>
                       )} */}
-            {/* </StatusParent> */}
-          </Link>
-        )}
-        {descendant && replies?.length > 0 && (
-          <SubComments
-            instance={instance}
-            replies={replies}
-            hasParentThread={thread}
-            level={1}
-            accWeight={weight}
-            openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
-          />
-        )}
-        {uiState === 'loading' &&
-          isHero &&
-          !!heroStatus?.repliesCount &&
-          !hasDescendants && (
-            <div class="status-loading">
-              <Loader />
-            </div>
+              {/* </StatusParent> */}
+            </Link>
           )}
-        {uiState === 'error' &&
-          isHero &&
-          !!heroStatus?.repliesCount &&
-          !hasDescendants && (
-            <div class="status-error">
-              Unable to load replies.
-              <br />
-              <button
-                type="button"
-                class="plain"
-                onClick={() => {
-                  states.reloadStatusPage++;
-                }}
-              >
-                Try again
-              </button>
-            </div>
+          {descendant && replies?.length > 0 && (
+            <SubComments
+              instance={instance}
+              replies={replies}
+              hasParentThread={thread}
+              level={1}
+              accWeight={weight}
+              openAll={totalDescendants.current < SUBCOMMENTS_OPEN_ALL_LIMIT}
+            />
           )}
-      </li>
-    );
-  };
+          {uiState === 'loading' &&
+            isHero &&
+            !!heroStatus?.repliesCount &&
+            !hasDescendants && (
+              <div class="status-loading">
+                <Loader />
+              </div>
+            )}
+          {uiState === 'error' &&
+            isHero &&
+            !!heroStatus?.repliesCount &&
+            !hasDescendants && (
+              <div class="status-error">
+                Unable to load replies.
+                <br />
+                <button
+                  type="button"
+                  class="plain"
+                  onClick={() => {
+                    states.reloadStatusPage++;
+                  }}
+                >
+                  Try again
+                </button>
+              </div>
+            )}
+        </li>
+      );
+    },
+    [
+      id,
+      instance,
+      uiState,
+      authenticated,
+      sameInstance,
+      translate,
+      handleMediaClick,
+      handleStatusLinkClick,
+      hasDescendants,
+    ],
+  );
+
+  const prevLocationIsStatusPage = useMemo(() => {
+    // Navigation API
+    if ('navigation' in window && navigation?.entries) {
+      const prevEntry = navigation.entries()[navigation.currentEntry.index - 1];
+      if (prevEntry?.url) {
+        return STATUS_URL_REGEX.test(prevEntry.url);
+      }
+    }
+    return STATUS_URL_REGEX.test(states.prevLocation?.pathname);
+  }, [sKey]);
 
   return (
     <div
@@ -876,7 +925,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
           </div> */}
         <div class="header-grid header-grid-2">
           <h1>
-            {!!/\/s\//i.test(snapStates.prevLocation?.pathname) && (
+            {prevLocationIsStatusPage && (
               <button
                 type="button"
                 class="plain deck-back"
@@ -970,7 +1019,7 @@ function StatusThread({ id, closeLink = '/', instance: propInstance }) {
           <div class="header-side">
             <button
               type="button"
-              class="plain4"
+              class="plain4 button-switch-view"
               style={{
                 display: viewMode === 'full' ? '' : 'none',
               }}
@@ -1231,11 +1280,11 @@ function SubComments({
             />
           ))}
         </span>
-        <span>
+        <b>
           <span title={replies.length}>{shortenNumber(replies.length)}</span>{' '}
           repl
           {replies.length === 1 ? 'y' : 'ies'}
-        </span>
+        </b>
         {!sameCount && totalComments > 1 && (
           <>
             {' '}
diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx
index dffd8310..c1126aa1 100644
--- a/src/pages/trending.jsx
+++ b/src/pages/trending.jsx
@@ -15,6 +15,7 @@ import { api } from '../utils/api';
 import { oklab2rgb, rgb2oklab } from '../utils/color-utils';
 import { filteredItems } from '../utils/filters';
 import pmem from '../utils/pmem';
+import shortenNumber from '../utils/shorten-number';
 import states from '../utils/states';
 import { saveStatus } from '../utils/states';
 import useTitle from '../utils/useTitle';
@@ -37,6 +38,7 @@ function Trending({ columnMode, ...props }) {
   const { masto, instance } = api({
     instance: props?.instance || params.instance,
   });
+  const { masto: currentMasto, instance: currentInstance } = api();
   const title = `Trending (${instance})`;
   useTitle(title, `/:instance?/trending`);
   // const navigate = useNavigate();
@@ -84,7 +86,7 @@ function Trending({ columnMode, ...props }) {
         latestItem.current = value[0].id;
       }
 
-      value = filteredItems(value, 'public'); // Might not work here
+      // value = filteredItems(value, 'public'); // Might not work here
       value.forEach((item) => {
         saveStatus(item, instance);
       });
@@ -131,7 +133,7 @@ function Trending({ columnMode, ...props }) {
                     <span class="more-insignificant">#</span>
                     {name}
                   </span>
-                  <span class="filter-count">{total.toLocaleString()}</span>
+                  <span class="filter-count">{shortenNumber(total)}</span>
                 </Link>
               );
             })}
@@ -241,7 +243,7 @@ function Trending({ columnMode, ...props }) {
       key={instance}
       title={title}
       titleComponent={
-        <h1 class="header-account">
+        <h1 class="header-double-lines">
           <b>Trending</b>
           <div>{instance}</div>
         </h1>
@@ -256,7 +258,8 @@ function Trending({ columnMode, ...props }) {
       useItemID
       headerStart={<></>}
       boostsCarousel={snapStates.settings.boostsCarousel}
-      allowFilters
+      // allowFilters
+      filterContext="public"
       timelineStart={TimelineStart}
       headerEnd={
         <Menu2
@@ -289,6 +292,18 @@ function Trending({ columnMode, ...props }) {
           >
             <Icon icon="bus" /> <span>Go to another instance…</span>
           </MenuItem>
+          {currentInstance !== instance && (
+            <MenuItem
+              onClick={() => {
+                location.hash = `/${currentInstance}/trending`;
+              }}
+            >
+              <Icon icon="bus" />{' '}
+              <small class="menu-double-lines">
+                Go to my instance (<b>{currentInstance}</b>)
+              </small>
+            </MenuItem>
+          )}
         </Menu2>
       }
     />
diff --git a/src/pages/welcome.css b/src/pages/welcome.css
index adb8fd45..1060d4b0 100644
--- a/src/pages/welcome.css
+++ b/src/pages/welcome.css
@@ -1,40 +1,3 @@
-#welcome {
-  text-align: center;
-  background-image: radial-gradient(
-      circle at center,
-      var(--bg-color),
-      transparent 16em
-    ),
-    radial-gradient(circle at center, var(--bg-color), transparent 8em);
-  background-repeat: no-repeat;
-  background-attachment: fixed;
-  padding: 16px;
-  cursor: default;
-}
-
-#welcome .hero-container {
-  padding-block: 60px;
-  height: 100vh;
-  height: 100svh;
-  max-height: 1024px;
-  display: flex;
-  flex-direction: column;
-}
-
-#welcome h1 {
-  margin: 0;
-  padding: 0;
-  font-size: 5em;
-  line-height: 1;
-  letter-spacing: -1px;
-  flex-grow: 1;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  position: relative;
-  mix-blend-mode: multiply;
-}
 @keyframes shine2 {
   0% {
     left: -100%;
@@ -46,92 +9,176 @@
     left: 100%;
   }
 }
-#welcome h1:before {
-  content: '';
-  position: absolute;
-  z-index: 2;
-  width: 100%;
-  height: 100%;
-  background-image: linear-gradient(
-    100deg,
-    rgba(255, 255, 255, 0) 30%,
-    rgba(255, 255, 255, 0.4),
-    rgba(255, 255, 255, 0) 70%
-  );
-  top: 0;
-  left: -100%;
-  pointer-events: none;
-  animation: shine2 5s ease-in-out 1s infinite;
-}
-@media (prefers-color-scheme: dark) {
-  #welcome {
+
+#welcome {
+  text-align: center;
+  background-image: radial-gradient(
+      circle at center,
+      var(--bg-color),
+      transparent 16em
+    ),
+    radial-gradient(circle at center, var(--bg-color), transparent 8em);
+  background-repeat: no-repeat;
+  background-attachment: fixed;
+  cursor: default;
+
+  @media (prefers-color-scheme: dark) {
     background-image: none;
   }
-  #welcome h1 {
-    mix-blend-mode: normal;
-  }
-  #welcome h1:before {
-    content: none;
-  }
-}
-#welcome img {
-  vertical-align: top;
-  transition: transform 0.3s ease-out;
-}
-#welcome h1 img {
-  filter: drop-shadow(-1px -1px var(--bg-blur-color))
-    drop-shadow(0 -1px 1px #fff)
-    drop-shadow(0 16px 32px var(--drop-shadow-color));
-}
-@media (prefers-color-scheme: dark) {
-  #welcome h1 img {
-    filter: none;
-  }
-}
 
-#welcome h1:hover img {
-  transform: scale(1.05);
-}
-#welcome .desc {
-  font-size: 1.4em;
-  text-wrap: balance;
-  opacity: 0.7;
-}
-#welcome .hero-container > p {
-  margin-top: 0;
-}
+  .hero-container {
+    padding: 16px;
+    height: 100vh;
+    height: 100svh;
+    max-height: 800px;
+    display: flex;
+    flex-direction: column;
 
-#why-container .sections {
-  padding-inline: 16px;
-}
-#why-container .sections section {
-  text-align: start;
-  max-width: 480px;
-  background-color: var(--bg-color);
-  border-radius: 16px;
-  overflow: hidden;
-  box-shadow: 17px 20px 40px var(--drop-shadow-color);
-  margin-bottom: 48px;
-}
-#why-container .sections section h4 {
-  margin: 0;
-  padding: 30px 30px 0;
-  font-size: 1.4em;
-  font-weight: 600;
-}
-#why-container .sections section p {
-  margin-inline: 30px;
-  margin-bottom: 30px;
-  opacity: 0.7;
-  text-wrap: balance;
-}
-#why-container .sections section img {
-  width: 100%;
-  height: auto;
-  border-bottom: 1px solid var(--outline-color);
-}
-@media (prefers-color-scheme: dark) {
-  #why-container .sections section img {
-    filter: invert(0.85) hue-rotate(180deg);
+    a {
+      color: inherit;
+
+      &:hover {
+        color: var(--link-text-color);
+      }
+    }
+  }
+
+  h1 {
+    margin: 0;
+    padding: 0;
+    font-size: 5em;
+    line-height: 1;
+    letter-spacing: -1px;
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+    mix-blend-mode: multiply;
+
+    @media (prefers-color-scheme: dark) {
+      mix-blend-mode: normal;
+    }
+
+    &:before {
+      content: '';
+      position: absolute;
+      z-index: 2;
+      width: 100%;
+      height: 100%;
+      background-image: linear-gradient(
+        100deg,
+        rgba(255, 255, 255, 0) 30%,
+        rgba(255, 255, 255, 0.4),
+        rgba(255, 255, 255, 0) 70%
+      );
+      top: 0;
+      left: -100%;
+      pointer-events: none;
+      animation: shine2 5s ease-in-out 1s infinite;
+
+      @media (prefers-color-scheme: dark) {
+        content: none;
+      }
+    }
+
+    img {
+      filter: drop-shadow(-1px -1px var(--bg-blur-color))
+        drop-shadow(0 -1px 1px #fff)
+        drop-shadow(0 16px 32px var(--drop-shadow-color));
+
+      @media (prefers-color-scheme: dark) {
+        filter: none;
+      }
+    }
+
+    &:hover img {
+      transform: scale(1.05);
+    }
+  }
+
+  img {
+    vertical-align: top;
+    transition: transform 0.3s ease-out;
+  }
+
+  .desc {
+    font-size: 1.4em;
+    text-wrap: balance;
+    opacity: 0.7;
+  }
+
+  .hero-container > p {
+    margin-top: 0;
+  }
+
+  #why-container {
+    padding: 0 16px;
+  }
+
+  .sections {
+    padding-inline: 16px;
+
+    section {
+      text-align: start;
+      max-width: 480px;
+      background-color: var(--bg-color);
+      border-radius: 16px;
+      overflow: hidden;
+      box-shadow: 17px 20px 40px var(--drop-shadow-color);
+      margin-bottom: 48px;
+
+      h4 {
+        margin: 0;
+        padding: 30px 30px 0;
+        font-size: 1.4em;
+        font-weight: 600;
+      }
+
+      p {
+        margin-inline: 30px;
+        margin-bottom: 30px;
+        opacity: 0.7;
+        text-wrap: balance;
+      }
+
+      img {
+        width: 100%;
+        height: auto;
+        border-bottom: 1px solid var(--outline-color);
+
+        @media (prefers-color-scheme: dark) {
+          filter: invert(0.85) hue-rotate(180deg);
+        }
+      }
+    }
+  }
+
+  @media (width > 40em) {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-template-rows: 1fr auto;
+    height: 100vh;
+    height: 100svh;
+
+    .hero-container {
+      height: auto;
+    }
+
+    #why-container {
+      padding: 32px;
+      overflow: auto;
+      mask-image: linear-gradient(to top, transparent 16px, black 64px);
+    }
+
+    footer {
+      grid-row: 2;
+      grid-column: 1 / span 2;
+    }
+  }
+
+  & ~ :is(#compose-button, #shortcuts) {
+    display: none;
   }
 }
diff --git a/src/pages/welcome.jsx b/src/pages/welcome.jsx
index e0abe16e..58882dcb 100644
--- a/src/pages/welcome.jsx
+++ b/src/pages/welcome.jsx
@@ -98,31 +98,33 @@ function Welcome() {
           </section>
         </div>
       </div>
-      <hr />
-      <p>
-        <a href="https://github.com/cheeaun/phanpy" target="_blank">
-          Built
-        </a>{' '}
-        by{' '}
-        <a
-          href="https://mastodon.social/@cheeaun"
-          target="_blank"
-          onClick={(e) => {
-            e.preventDefault();
-            states.showAccount = 'cheeaun@mastodon.social';
-          }}
-        >
-          @cheeaun
-        </a>
-        .{' '}
-        <a
-          href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
-          target="_blank"
-        >
-          Privacy Policy
-        </a>
-        .
-      </p>
+      <footer>
+        <hr />
+        <p>
+          <a href="https://github.com/cheeaun/phanpy" target="_blank">
+            Built
+          </a>{' '}
+          by{' '}
+          <a
+            href="https://mastodon.social/@cheeaun"
+            target="_blank"
+            onClick={(e) => {
+              e.preventDefault();
+              states.showAccount = 'cheeaun@mastodon.social';
+            }}
+          >
+            @cheeaun
+          </a>
+          .{' '}
+          <a
+            href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
+            target="_blank"
+          >
+            Privacy Policy
+          </a>
+          .
+        </p>
+      </footer>
     </main>
   );
 }
diff --git a/src/utils/api.js b/src/utils/api.js
index debc4422..2d101915 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -221,8 +221,34 @@ export function api({ instance, accessToken, accountID, account } = {}) {
     }
   }
 
+  const currentAccount = getCurrentAccount();
+
   // If only instance is provided, get the masto instance for that instance
   if (instance) {
+    if (currentAccountApi?.instance === instance) {
+      return {
+        masto: currentAccountApi.masto,
+        streaming: currentAccountApi.streaming,
+        client: currentAccountApi,
+        authenticated: true,
+        instance,
+      };
+    }
+
+    if (currentAccount?.instanceURL === instance) {
+      const { accessToken } = currentAccount;
+      currentAccountApi =
+        accountApis[instance]?.[accessToken] ||
+        initClient({ instance, accessToken });
+      return {
+        masto: currentAccountApi.masto,
+        streaming: currentAccountApi.streaming,
+        client: currentAccountApi,
+        authenticated: true,
+        instance,
+      };
+    }
+
     const client = apis[instance] || initClient({ instance });
     const { masto, streaming, accessToken } = client;
     return {
@@ -244,7 +270,6 @@ export function api({ instance, accessToken, accountID, account } = {}) {
       instance: currentAccountApi.instance,
     };
   }
-  const currentAccount = getCurrentAccount();
   if (currentAccount) {
     const { accessToken, instanceURL: instance } = currentAccount;
     currentAccountApi =
diff --git a/src/utils/filter-context.js b/src/utils/filter-context.js
new file mode 100644
index 00000000..f896690a
--- /dev/null
+++ b/src/utils/filter-context.js
@@ -0,0 +1,4 @@
+import { createContext } from 'preact';
+
+const FilterContext = createContext();
+export default FilterContext;
diff --git a/src/utils/filters.jsx b/src/utils/filters.jsx
index 18f6877f..5d51bc0b 100644
--- a/src/utils/filters.jsx
+++ b/src/utils/filters.jsx
@@ -1,10 +1,8 @@
+import mem from './mem';
 import store from './store';
 
-export function filteredItem(item, filterContext, currentAccountID) {
-  const { filtered } = item;
-  if (!filtered?.length) return true;
-  const isSelf = currentAccountID && item.account?.id === currentAccountID;
-  if (isSelf) return true;
+function _isFiltered(filtered, filterContext) {
+  if (!filtered?.length) return false;
   const appliedFilters = filtered.filter((f) => {
     const { filter } = f;
     const hasContext = filter.context.includes(filterContext);
@@ -12,19 +10,35 @@ export function filteredItem(item, filterContext, currentAccountID) {
     if (!filter.expiresAt) return hasContext;
     return new Date(filter.expiresAt) > new Date();
   });
-  if (!appliedFilters.length) return true;
+  if (!appliedFilters.length) return false;
   const isHidden = appliedFilters.some((f) => f.filter.filterAction === 'hide');
-  console.log({ isHidden, filtered, appliedFilters, item });
-  if (isHidden) return false;
+  if (isHidden)
+    return {
+      action: 'hide',
+    };
   const isWarn = appliedFilters.some((f) => f.filter.filterAction === 'warn');
   if (isWarn) {
     const filterTitles = appliedFilters.map((f) => f.filter.title);
-    item._filtered = {
+    return {
+      action: 'warn',
       titles: filterTitles,
       titlesStr: filterTitles.join(' • '),
     };
   }
-  return isWarn;
+  return false;
+}
+export const isFiltered = mem(_isFiltered);
+
+export function filteredItem(item, filterContext, currentAccountID) {
+  const { filtered } = item;
+  if (!filtered?.length) return true;
+  const isSelf = currentAccountID && item.account?.id === currentAccountID;
+  if (isSelf) return true;
+  const filterState = isFiltered(filtered, filterContext);
+  if (!filterState) return true;
+  if (filterState.action === 'hide') return false;
+  // item._filtered = filterState;
+  return true;
 }
 export function filteredItems(items, filterContext) {
   if (!items?.length) return [];
diff --git a/src/utils/group-notifications.jsx b/src/utils/group-notifications.jsx
index 83326ccb..132fd32a 100644
--- a/src/utils/group-notifications.jsx
+++ b/src/utils/group-notifications.jsx
@@ -1,4 +1,37 @@
+// This is like very lame "type-checking" lol
+const notificationTypeKeys = {
+  mention: ['account', 'status'],
+  status: ['account', 'status'],
+  reblog: ['account', 'status'],
+  follow: ['account'],
+  follow_request: ['account'],
+  favourite: ['account', 'status'],
+  poll: ['status'],
+  update: ['status'],
+};
+function fixNotifications(notifications) {
+  return notifications.filter((notification) => {
+    const { type, id, createdAt } = notification;
+    if (!type) {
+      console.warn('Notification missing type', notification);
+      return false;
+    }
+    if (!id || !createdAt) {
+      console.warn('Notification missing id or createdAt', notification);
+      // Continue processing this despite missing id or createdAt
+    }
+    const keys = notificationTypeKeys[type];
+    if (keys?.length) {
+      return keys.every((key) => !!notification[key]);
+    }
+    return true; // skip other types
+  });
+}
+
 function groupNotifications(notifications) {
+  // Filter out invalid notifications
+  notifications = fixNotifications(notifications);
+
   // Create new flat list of notifications
   // Combine sibling notifications based on type and status id
   // Concat all notification.account into an array of _accounts
@@ -7,7 +40,7 @@ function groupNotifications(notifications) {
   for (let i = 0, j = 0; i < notifications.length; i++) {
     const notification = notifications[i];
     const { id, status, account, type, createdAt } = notification;
-    const date = new Date(createdAt).toLocaleDateString();
+    const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
     let virtualType = type;
     if (type === 'favourite' || type === 'reblog') {
       virtualType = 'favourite+reblog';
@@ -50,7 +83,7 @@ function groupNotifications(notifications) {
   for (let i = 0, j = 0; i < cleanNotifications.length; i++) {
     const notification = cleanNotifications[i];
     const { id, account, _accounts, type, createdAt } = notification;
-    const date = new Date(createdAt).toLocaleDateString();
+    const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
     if (type === 'favourite+reblog' && account && _accounts.length === 1) {
       const key = `${account?.id}-${type}-${date}`;
       const mappedNotification = notificationsMap2[key];
diff --git a/src/utils/isMastodonLinkMaybe.jsx b/src/utils/isMastodonLinkMaybe.jsx
index 7e029d2a..8fcfb306 100644
--- a/src/utils/isMastodonLinkMaybe.jsx
+++ b/src/utils/isMastodonLinkMaybe.jsx
@@ -1,9 +1,11 @@
 export default function isMastodonLinkMaybe(url) {
-  const { pathname } = new URL(url);
+  const { pathname, hash } = new URL(url);
   return (
     /^\/.*\/\d+$/i.test(pathname) ||
     /^\/@[^/]+\/(statuses|posts)\/\w+\/?$/i.test(pathname) || // GoToSocial, Takahe
     /^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
-    /^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) // Pleroma
+    /^\/notes\/[a-z0-9]+$/i.test(pathname) || // Misskey, Calckey
+    /^\/(notice|objects)\/[a-z0-9-]+$/i.test(pathname) || // Pleroma
+    /#\/[^\/]+\.[^\/]+\/s\/.+/i.test(hash) // Phanpy 🫣
   );
 }
diff --git a/src/utils/mem.js b/src/utils/mem.js
index 773a4929..620ab6df 100644
--- a/src/utils/mem.js
+++ b/src/utils/mem.js
@@ -1,5 +1,7 @@
 import moize from 'moize';
 
+window._moize = moize;
+
 export default function mem(fn, opts = {}) {
-  return moize(fn, { ...opts, maxSize: 100 });
+  return moize(fn, { ...opts, maxSize: 50, isDeepEqual: true });
 }
diff --git a/src/utils/open-compose.js b/src/utils/open-compose.js
index da677112..b8192876 100644
--- a/src/utils/open-compose.js
+++ b/src/utils/open-compose.js
@@ -18,6 +18,8 @@ export default function openCompose(opts) {
     // }
 
     newWin.__COMPOSE__ = opts;
+  } else {
+    alert('Looks like your browser is blocking popups.');
   }
 
   return newWin;
diff --git a/src/utils/states.js b/src/utils/states.js
index 580d02aa..314af7fb 100644
--- a/src/utils/states.js
+++ b/src/utils/states.js
@@ -22,7 +22,6 @@ const states = proxy({
   notificationsNew: [],
   notificationsShowNew: false,
   notificationsLastFetchTime: null,
-  accounts: {},
   reloadStatusPage: 0,
   reloadGenericAccounts: {
     id: null,
@@ -72,8 +71,9 @@ export function initStates() {
     store.account.get('settings-autoRefresh') ?? false;
   states.settings.shortcutsViewMode =
     store.account.get('settings-shortcutsViewMode') ?? null;
-  states.settings.shortcutsColumnsMode =
-    store.account.get('settings-shortcutsColumnsMode') ?? false;
+  if (store.account.get('settings-shortcutsColumnsMode')) {
+    states.settings.shortcutsColumnsMode = true;
+  }
   states.settings.boostsCarousel =
     store.account.get('settings-boostsCarousel') ?? true;
   states.settings.contentTranslation =
@@ -100,9 +100,6 @@ subscribe(states, (changes) => {
     if (path.join('.') === 'settings.boostsCarousel') {
       store.account.set('settings-boostsCarousel', !!value);
     }
-    if (path.join('.') === 'settings.shortcutsColumnsMode') {
-      store.account.set('settings-shortcutsColumnsMode', !!value);
-    }
     if (path.join('.') === 'settings.shortcutsViewMode') {
       store.account.set('settings-shortcutsViewMode', value);
     }
@@ -171,7 +168,7 @@ export function saveStatus(status, instance, opts) {
   if (!override && oldStatus) return;
   const key = statusKey(status.id, instance);
   if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
-  if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
+  // if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
   states.statuses[key] = status;
   if (status.reblog) {
     const key = statusKey(status.reblog.id, instance);
diff --git a/src/utils/supports.js b/src/utils/supports.js
index 5f7e8163..02b1a496 100644
--- a/src/utils/supports.js
+++ b/src/utils/supports.js
@@ -1,4 +1,4 @@
-import { satisfies } from 'semver';
+import { satisfies } from 'compare-versions';
 
 import features from '../data/features.json';