From ee417df60ebb81838459e0db324c79e61c906b53 Mon Sep 17 00:00:00 2001
From: Balazs Nadasdi <yitsushi@protonmail.ch>
Date: Sat, 19 Jan 2019 18:38:17 +0100
Subject: [PATCH] Status ID + thread view

 - Status ID on timeline list view
 - thread command to view a complete thread
   Display order:
    - ancestors
    - status
    - descendants
---
 tests/test_console.py | 89 +++++++++++++++++++++++++++++++++++++++++++
 toot/api.py           | 18 +++++++--
 toot/commands.py      | 13 +++++++
 toot/console.py       | 10 +++++
 toot/output.py        |  7 ++++
 5 files changed, 134 insertions(+), 3 deletions(-)

diff --git a/tests/test_console.py b/tests/test_console.py
index 3f28e04..53bb5e6 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -121,6 +121,7 @@ def test_delete(mock_delete, capsys):
 @mock.patch('toot.http.get')
 def test_timeline(mock_get, monkeypatch, capsys):
     mock_get.return_value = MockResponse([{
+        'id': '111111111111111111',
         'account': {
             'display_name': 'Frank Zappa',
             'username': 'fz'
@@ -128,6 +129,7 @@ def test_timeline(mock_get, monkeypatch, capsys):
         'created_at': '2017-04-12T15:53:18.174Z',
         'content': "<p>The computer can&apos;t tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.</p>",
         'reblog': None,
+        'in_reply_to_id': None
     }])
 
     console.run_command(app, user, 'timeline', [])
@@ -139,7 +141,94 @@ def test_timeline(mock_get, monkeypatch, capsys):
     assert "but what's missing is the eyebrows." in out
     assert "Frank Zappa" in out
     assert "@fz" in out
+    assert "id: 111111111111111111" in out
+    assert "[RE]" not in out
 
+@mock.patch('toot.http.get')
+def test_timeline_with_re(mock_get, monkeypatch, capsys):
+    mock_get.return_value = MockResponse([{
+        'id': '111111111111111111',
+        'account': {
+            'display_name': 'Frank Zappa',
+            'username': 'fz'
+        },
+        'created_at': '2017-04-12T15:53:18.174Z',
+        'content': "<p>The computer can&apos;t tell you the emotional story. It can give you the exact mathematical design, but what's missing is the eyebrows.</p>",
+        'reblog': None,
+        'in_reply_to_id': '111111111111111110'
+    }])
+
+    console.run_command(app, user, 'timeline', [])
+
+    mock_get.assert_called_once_with(app, user, '/api/v1/timelines/home')
+
+    out, err = capsys.readouterr()
+    assert "The computer can't tell you the emotional story." in out
+    assert "but what's missing is the eyebrows." in out
+    assert "Frank Zappa" in out
+    assert "@fz" in out
+    assert "id: 111111111111111111" in out
+    assert "[RE]" in out
+
+@mock.patch('toot.http.get')
+def test_thread(mock_get, monkeypatch, capsys):
+    mock_get.side_effect = [
+        MockResponse({
+            'id': '111111111111111111',
+            'account': {
+                'display_name': 'Frank Zappa',
+                'username': 'fz'
+            },
+            'created_at': '2017-04-12T15:53:18.174Z',
+            'content': "my response in the middle",
+            'reblog': None,
+            'in_reply_to_id': '111111111111111110'
+        }),
+        MockResponse({
+            'ancestors': [{
+                'id': '111111111111111110',
+                'account': {
+                    'display_name': 'Frank Zappa',
+                    'username': 'fz'
+                },
+                'created_at': '2017-04-12T15:53:18.174Z',
+                'content': "original content",
+                'reblog': None,
+                'in_reply_to_id': None}],
+            'descendants': [{
+                'id': '111111111111111112',
+                'account': {
+                    'display_name': 'Frank Zappa',
+                    'username': 'fz'
+                },
+                'created_at': '2017-04-12T15:53:18.174Z',
+                'content': "response message",
+                'reblog': None,
+                'in_reply_to_id': '111111111111111111'}],
+        }),
+    ]
+
+    console.run_command(app, user, 'thread', ['111111111111111111'])
+
+    calls = [
+        mock.call(app, user, '/api/v1/statuses/111111111111111111'),
+        mock.call(app, user, '/api/v1/statuses/111111111111111111/context'),
+    ]
+    mock_get.assert_has_calls(calls, any_order=False)
+
+    out, err = capsys.readouterr()
+
+    # Display order
+    assert out.index('original content') < out.index('my response in the middle')
+    assert out.index('my response in the middle') < out.index('response message')
+
+    assert "original content" in out
+    assert "my response in the middle" in out
+    assert "response message" in out
+    assert "Frank Zappa" in out
+    assert "@fz" in out
+    assert "id: 111111111111111111" in out
+    assert "[RE]" in out
 
 @mock.patch('toot.http.post')
 def test_upload(mock_post, capsys):
diff --git a/toot/api.py b/toot/api.py
index e0cca77..e302f84 100644
--- a/toot/api.py
+++ b/toot/api.py
@@ -17,10 +17,17 @@ def _account_action(app, user, account, action):
     return http.post(app, user, url).json()
 
 
-def _status_action(app, user, status_id, action):
-    url = '/api/v1/statuses/{}/{}'.format(status_id, action)
+def _status_action(app, user, status_id, action, method='post'):
+    if action is None:
+        url = '/api/v1/statuses/{}'.format(status_id)
+        method = 'get'
+    else:
+        url = '/api/v1/statuses/{}/{}'.format(status_id, action)
 
-    return http.post(app, user, url).json()
+    if method == 'post':
+        return http.post(app, user, url).json()
+    elif method == 'get':
+        return http.get(app, user, url).json()
 
 
 def create_app(domain, scheme='https'):
@@ -143,6 +150,8 @@ def pin(app, user, status_id):
 def unpin(app, user, status_id):
     return _status_action(app, user, status_id, 'unpin')
 
+def context(app, user, status_id):
+    return _status_action(app, user, status_id, 'context', method='get')
 
 def timeline_home(app, user):
     return http.get(app, user, '/api/v1/timelines/home').json()
@@ -245,6 +254,9 @@ def verify_credentials(app, user):
     return http.get(app, user, '/api/v1/accounts/verify_credentials').json()
 
 
+def single_status(app, user, status_id):
+    return _status_action(app, user, status_id, None, method='get')
+
 def get_notifications(app, user):
     return http.get(app, user, '/api/v1/notifications').json()
 
diff --git a/toot/commands.py b/toot/commands.py
index 4d9342b..9542f04 100644
--- a/toot/commands.py
+++ b/toot/commands.py
@@ -29,6 +29,19 @@ def timeline(app, user, args):
 
     print_timeline(items)
 
+def thread(app, user, args):
+    toot = api.single_status(app, user, args.status_id)
+    context = api.context(app, user, args.status_id)
+    thread = []
+    for item in context['ancestors']:
+        thread.append(item)
+
+    thread.append(toot)
+
+    for item in context['descendants']:
+        thread.append(item)
+
+    print_timeline(thread)
 
 def curses(app, user, args):
     from toot.ui.app import TimelineApp
diff --git a/toot/console.py b/toot/console.py
index c6e5363..5b45eff 100644
--- a/toot/console.py
+++ b/toot/console.py
@@ -154,6 +154,16 @@ READ_COMMANDS = [
         ],
         require_auth=True,
     ),
+    Command(
+        name="thread",
+        description="Show toot thfread items",
+        arguments=[
+            (["status_id"], {
+                "help": "Show thread for toot.",
+            }),
+        ],
+        require_auth=True,
+    ),
     Command(
         name="timeline",
         description="Show recent items in a timeline (home by default)",
diff --git a/toot/output.py b/toot/output.py
index 36fec5f..60c034d 100644
--- a/toot/output.py
+++ b/toot/output.py
@@ -137,6 +137,11 @@ def print_timeline(items):
             if item['reblogged']:
                 left_column.append("Reblogged @{}".format(item['reblogged']))
 
+            if item['reply_to_toot'] is not None:
+                left_column.append('[RE]')
+
+            left_column.append("id: {}".format(item['id']))
+
             right_column = wrap_text(item['text'], 80)
 
             return zip_longest(left_column, right_column, fillvalue="")
@@ -153,10 +158,12 @@ def print_timeline(items):
         time = datetime.strptime(item['created_at'], "%Y-%m-%dT%H:%M:%S.%fZ")
 
         return {
+            "id": item['id'],
             "account": item['account'],
             "text": text,
             "time": time,
             "reblogged": reblogged,
+            "reply_to_toot": item['in_reply_to_id']
         }
 
     print_out("─" * 31 + "┬" + "─" * 88)