Twitterアプリの改悪にはウンザリだ！！！そうだ！！！Pythonista3からTweetDeckを閲覧しよう！！！！！

この記事は、[Pythonista3 Advent Calendar 2022](https://qiita.com/advent-calendar/2022/pythonista3) の25日目の記事です。

https://qiita.com/advent-calendar/2022/pythonista3

# Introduction

本記事では、TweetDeckをスマホ版UI/UXで、Pythonista3から閲覧する方法を紹介します。

## 環境 / 前提

- iOS 16.1.1
- Pythonista3 v3.3

Python自体の文法は扱いませんが、コードの解説は気持ち程度行います。

## 背景: Twitterのインプレッション可視化問題

2022年12月下旬某日、Twitterのトレンドは「インプレッション」「閲覧数」「可視化」「欲求承認モンスター」etc...とスマホ版Twitterアプリが追加したとある機能で物議を醸していた！！！

先日、Twitterアプリではツイートの左下にインプレッションの数(?)が**誰でも**見れるようになりました。
これにより大きくは2つの不満がTLでは散見されました。

- いいねの数とインプレッションの差で悲しくなる
- 従来のリプライ, RT, いいねの位置がずれてしまって不便

個人的には後者の方が嫌ですね。
まぁ~~Twitterが要らない機能を加えるのはよくあることなので~~どうせ慣れる気はします。
とはいえ、他のクライアントアプリ[^1]を使ってみたいなぁと思い調査してみました。
そこでクライアントアプリの代表ともいえる[TweetDeck](https://tweetdeck.twitter.com)なるものに出会いました。

https://tweetdeck.twitter.com

Chrome拡張機能の[Better TweetDeck](https://chrome.google.com/webstore/detail/better-tweetdeck/micblkellenpbfapmcpcfhcoeohhnpob?hl=en), [MultiRow TweetDeck](https://chrome.google.com/webstore/detail/multirow-tweetdeck/cjlaagghmikageagedknpkmapcjodnno?hl=ja)などと組み合わせると、PCでは色々とできてよい感じがします。

[^1]: 単にTwitterを閲覧するアプリという意味合いです。

## MTDeck - TweetDeckをスマホアプリ化

しかしながらTweetDeck本家は、**スマホ版には対応していない**とのこと[^2]。
「TweetDeck スマホ」で${}^\dagger$Google 検索${}^\dagger$してみると、次のユーザースクリプトが見つかりました。

https://github.com/mkizka/MTDeck

こちらのスクリプトをTweetDeckのページで読み込ませることで、スマホアプリのようなUI/UXを実現することができます。
[TweetDeckをスマホアプリ化するユーザースクリプト、MTDeckの導入方法を詳しく解説](https://yonoi.com/tjdeck-and-mtdeck/)などでは、一例としてAndroidではKiwi Browser, iOSではOhajiki Web BrowserやSafari Snippetsを紹介しています。

[^2]: MarinDeckなど、TweetDeckをスマホアプリにする優れたアプリはあります。素晴らしい試みでありますが、個人的には少し読み込み時間が長い点が気になります。

TweetDeck + MTDeckを実現するためには、必要な要件は次の2点になります。

1. ブラウザでTweetDeckのページが開ける
1. 開いていたページで用意したJavaScriptが読み込める

天才ワイ「**これPythonista3でオッケーちゃうか？**」
続いてPythonista3について軽く紹介します。

## Pythonista3

Pythonista3とは、スマホでPythonを動かせる実行環境を兼ね備えたアプリです。
入力補完をはじめとしたコーディング体験, 文字の直感的な1文字単位のスライドなど、大きなところから小さなところまで様々な工夫が凝らされた、「僕が考えた最強のPythonコーディング環境 on スマホ」です。
簡易なアプリ・ゲーム作成, キーボード拡張, Widget作成まで可能とする、もはや意味不明(褒め言葉)なアプリです。
少しでも興味を持ったらタグやアドカレをご覧ください。

https://qiita.com/tags/pythonista3


### uiモジュール

Pythonista3では、uiモジュールという簡単にGUIアプリを作れるフレームワーク的なものが用意されています。

https://omz-software.com/pythonista/docs/ios/ui.html

`ui.WebView`は指定したhtml, またURLでページを表示するコンポーネントなので、これを使うことで外部サイトの閲覧が可能です。
しかしさすがにJavaScriptを読み込ませるのは...

>WebView.eval_js(js)
WebView.evaluate_javascript(js)
Evaluate a snippet of JavaScript in the context of the current page and return the result as a string.

**ありました**。何気に返り値も取得可能なので、夢が広がります。
というわけで、この機能を用いてTweetDeck on Pythonista3を実現します。

# TweetDeck on Pythoniosta3

## DEMO

最小構成 (+ ちょっとだけ便利なメソッド )の成果物を示します。
↓ 実際の画面 ↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483769/5dd0ce94-d5d7-0ea3-a3bb-c58c9f1b54cf.png)
↑ 実際の画面 ↑

## コード

mtdeck.jsをPythonista3に追加します。
中身は[MTdeck/dist/mtdeck.user.js](https://github.com/mkizka/MTDeck/blob/master/dist/mtdeck.user.js)をコピーして貼り付けます。

```
TweetDeckOnPythonista3/
 |- main.py
 |- mtdeck.js
```

```main.py
import ui
import time

TweetDeckUrl = 'https://tweetdeck.twitter.com/'

def getJavaScript(path):
	with open(path, 'r', encoding='utf-8') as f:
		s = ''.join(f.readlines())
	return s

class WebviewDelegate:
	def webview_should_start_load(self, webview, url, nav_type):
		return True
	def webview_did_start_load(self, webview):
		self.js = getJavaScript('mtdeck.js')
	
	def webview_did_finish_load(self, webview):
		webview.eval_js(self.js)

	def webview_did_fail_load(self, webview, error_code, error_msg):
		pass

class TweetDeckBrowser:
	def __init__(self):
		self.webview = ui.WebView()
        # Delegateを利用して遅延してJSを読み込ませる
		self.webview.delegate = WebviewDelegate()
        # 拡大縮小の操作をしない
		self.webview.scales_page_to_fit = False
		
		self.load()
	
	def load(self, *args):
		self.webview.load_url(TweetDeckUrl)
	
	def closeSideMenu(self, *args):
		js = '''if(!document.body.classList.contains("mtdeck-close")) { document.body.classList.add("mtdeck-close") }'''
		self.webview.eval_js(js)
	
	def goToPreviousColumn(self, *args):
		js = 'window.MTD.backColumn()'
		self.webview.eval_js(js)
	
	def goToNextColumn(self, *args):
		js = 'window.MTD.pushColumn()'
		self.webview.eval_js(js)

class TweetDeckApp(ui.View):
	def __init__(self):
		super().__init__()
		
		self.background_color = 'gray'
		
		self.browser = TweetDeckBrowser()
		self.add_subview(self.browser.webview)
		
		# 画面上部のボタン定義
		reloadBtn = ui.ButtonItem()
		reloadBtn.image = ui.Image.named('iob:ios7_reload_32')
		
		@ui.in_background
		def reload(*args):
			reloadBtn.enabled = False
			self.browser.webview.reload()
            time.sleep(1)
			reloadBtn.enabled = True
		
		reloadBtn.action = reload
		self.right_button_items = [
			reloadBtn
		]
	
	def layout(self):
		_, _, w, h = self.frame
		footer_height = h/15
		# 全画面使うとサイトの下の方が見切れるので。
		self.browser.webview.frame = (0, 0, w, h - footer_height)
		
	
if __name__ == '__main__':
	app = TweetDeckApp()
	app.present('fullscreen')
```

### `layout`メソッドとは

こちらが詳しいです。というか`ui.View`のカスタムViewの入門として最強だと思います。これ読みましょう。

https://qiita.com/pome-ta/items/2f02d94eea73974aae88

### `TweetDeckBrowser`について

`ui.WebView`をラップするようなクラスです。(サブクラス化させたかったけど禁止されている模様)
ここでポイントとして、JavaScriptの読み込みタイミングです。このスクリプトはサイト(HTML)が読み込まれた後に読み込む必要があります。本記事では、Delegateを用いて解決しています。

#### delegate

(少なくともuiモジュールでは)Delegateとは処理をお願いするオブジェクトのことで、`ui.WebView`の場合は4つのメソッドを持つオブジェクトが該当します。

```main.py
class WebviewDelegate:
	def webview_should_start_load(self, webview, url, nav_type):
		return True
    def webview_did_start_load(self, webview):  # 1
		self.js = getJavaScript('mtdeck.js')
	
	def webview_did_finish_load(self, webview):  # 2
		webview.eval_js(self.js)

	def webview_did_fail_load(self, webview, error_code, error_msg):
		pass
```

1. `webview_did_start_load`: ページのロードが始まる直前にスクリプトを用意して
1. `webview_did_finish_load`: ロードが終わったら実行する

という動作を実装しています。

#### `TweetDeckBrowser.goToPreviousColumn, TweetDeckBrowser.goToNextColumn`

列を動的に切り替えます。
ここはJavaScriptの話になります。
MTDeckにはドキュメントなどはありませんが、[コードの688行目](https://github.com/mkizka/MTDeck/blob/d7285c3f0b6c04de940bbea046434a0974395302/dist/mtdeck.user.js#L688)をよく見ると、`window.MTD`として外側に露出させているクラスがあります。
このクラス`Deck`は`Deck.pushColumn(), Deck.backColumn()`を持っており、このメソッドを使うことで画面に表示しているカラムを変えています。つまり、このメソッドをPython側から呼び出すことができれば、GUI側からカラムの変更を行えます。

このメソッドは単なる例であり、今回の実装ではどこからも呼び出していませんが、`ui.WebView.eval_js()`のお陰でこのようなことが可能であるというお話です。
`TweetDeckBrowser.closeSideMenu`もJSのコードを参考にして、サイドバーを閉じる処理を書いています。
可能性を感じませんか？？？

### 右上のボタン追加 : `ui.ButtonItem`, `right_button_items`

~~よく考えたらこの辺の部品の日本語の説明見たことがないですね。そのうち記事を書くかもしれません。~~

今回の実装では、画面の右上にボタンを追加しています。
リロードするボタンを追加してみました。

```main.py
class TweetDeckApp(ui.View):
	def __init__(self):
		super().__init__()
        ...
    
        reloadBtn = ui.ButtonItem()
        reloadBtn.image = ui.Image.named('iob:ios7_reload_32')

        ...
    
        self.right_button_items = [
            reloadBtn
        ]
```

`@ui.in_background`については...

([自分の記事](https://qiita.com/take_me/items/1c2ad512ec3ee9a8bdf4)より引用)

>uiなどのGUI表示部分の処理とは異なるスレッドで処理させるデコレーションです。多分。
ボタンのactionなどのコールバック関数にデコレーションをすると、別スレッドで処理が走ります。
よくわかりゃん！！の場合は、下記のようにアクションを行う部分に@でつけるとだけ覚えておけばよいです。

```python
import ui

@ui.in_background
def btn_action(sender):
    # 何か重い処理
    pass

...    # ui.Buttonのactionに`btn_action`を登録する
```

>デコレータについて詳しくなりたい方は、[Pythonのデコレータについて](https://qiita.com/mtb_beta/items/d257519b018b8cd0cc2e)を参照してください。

# Conclusion

本記事では、TweetDeck(+MTDeck)をPythonista3で動かす方法を紹介しました。
Pythoista3で動かすことで、次のメリットがあります。
- Python, JavaScript, HTML, CSSの理解が深まる
- uiモジュールの理解が深まる
- GUIからJSを実行することで、挙動を改造できる
- CSSを簡単にinjectできる (今回の実装では触れていません。)
Pythonista3自体が有料ではありますが、Pythonを書く人はもれなく購入すべきアプリですので、これを機会にお試しあれ！

# Reference

https://yonoi.com/tjdeck-and-mtdeck/

https://qiita.com/pome-ta/items/2f02d94eea73974aae88

https://qiita.com/take_me/items/1c2ad512ec3ee9a8bdf4

# Appendix

## Discord

Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。

https://t.co/0IOW2hn1VC