mirror of
https://github.com/ihabunek/toot.git
synced 2024-09-29 04:35:54 -04:00
Merge branch 'master' into images
This commit is contained in:
commit
b94c500c9c
46
CHANGELOG.md
46
CHANGELOG.md
@ -3,20 +3,42 @@ Changelog
|
||||
|
||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||
|
||||
**0.40.0 (TBA)**
|
||||
**0.41.1 (2024-01-02)**
|
||||
|
||||
This release includes a major rewrite to use
|
||||
[Click](https://click.palletsprojects.com/) for creating the command line
|
||||
interface. This allows for some new features like nested commands, setting
|
||||
parameters via environment variables, and shell completion. See docs for
|
||||
details. Backward compatibility should be mostly preserved, except for cases
|
||||
noted below please report any issues.
|
||||
* Fix a crash in settings parsing code
|
||||
|
||||
**0.41.0 (2024-01-02)**
|
||||
|
||||
* Honour user's default visibility set in Mastodon preferences instead of always
|
||||
defaulting to public visibility (thanks Lexi Winter)
|
||||
* TUI: Add editing toots (thanks Lexi Winter)
|
||||
* TUI: Fix a bug which made pallette config in settings not work
|
||||
* TUI: Show edit datetime in status detail (thanks Lexi Winter)
|
||||
|
||||
**0.40.2 (2023-12-28)**
|
||||
|
||||
* Reinstate `toot post --using` option.
|
||||
* Add shell completion for instances.
|
||||
|
||||
**0.40.1 (2023-12-28)**
|
||||
|
||||
* Add `toot --as` option to replace `toot post --using`. This now works for all
|
||||
commands.
|
||||
|
||||
**0.40.0 (2023-12-27)**
|
||||
|
||||
This release includes a rather extensive change to use the Click library
|
||||
(https://click.palletsprojects.com/) for creating the command line interface.
|
||||
This allows for some new features like nested commands, setting parameters via
|
||||
environment variables, and shell completion. Backward compatibility should be
|
||||
mostly preserved, except for cases noted below. Please report any issues.
|
||||
|
||||
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
||||
`login_cli`, pass the base URL instead
|
||||
* BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after
|
||||
`toot` but before the command
|
||||
* Enable passing params via environment variables, see:
|
||||
* BREAKING: Options `--debug` and `--color` must be specified after `toot` but
|
||||
before the command
|
||||
* BREAKING: Option `--quiet` has been removed. Redirect output instead.
|
||||
* Add passing parameters via environment variables, see:
|
||||
https://toot.bezdomni.net/environment_variables.html
|
||||
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
||||
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
||||
@ -26,11 +48,11 @@ noted below please report any issues.
|
||||
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
||||
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
||||
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
||||
* Add `--json` option to tags commands
|
||||
* Add `--json` option to lists commands
|
||||
* Add `--json` option to tags and lists commands
|
||||
* Add `toot --width` option for setting your prefered terminal width
|
||||
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
||||
previously accessible only via settings.
|
||||
* TUI: Fix issue where UI did not render until first input (thanks Urwid devs)
|
||||
|
||||
**0.39.0 (2023-11-23)**
|
||||
|
||||
|
@ -1,23 +1,49 @@
|
||||
0.41.1:
|
||||
date: 2024-01-02
|
||||
changes:
|
||||
- "Fix a crash in settings parsing code"
|
||||
|
||||
0.41.0:
|
||||
date: 2024-01-02
|
||||
changes:
|
||||
- "Honour user's default visibility set in Mastodon preferences instead of always defaulting to public visibility (thanks Lexi Winter)"
|
||||
- "TUI: Add editing toots (thanks Lexi Winter)"
|
||||
- "TUI: Fix a bug which made pallette config in settings not work"
|
||||
- "TUI: Show edit datetime in status detail (thanks Lexi Winter)"
|
||||
|
||||
0.40.2:
|
||||
date: 2023-12-28
|
||||
changes:
|
||||
- "Reinstate `toot post --using` option."
|
||||
- "Add shell completion for instances."
|
||||
|
||||
0.40.1:
|
||||
date: 2023-12-28
|
||||
changes:
|
||||
- "Add `toot --as` option to replace `toot post --using`. This now works for all commands."
|
||||
|
||||
0.40.0:
|
||||
date: TBA
|
||||
date: 2023-12-27
|
||||
description: |
|
||||
This release includes a major rewrite to use [Click](https://click.palletsprojects.com/) for
|
||||
creating the command line interface. This allows for some new features like nested commands,
|
||||
setting parameters via environment variables, and shell completion. See docs for details.
|
||||
Backward compatibility should be mostly preserved, except for cases noted below please report
|
||||
any issues.
|
||||
This release includes a rather extensive change to use the Click library
|
||||
(https://click.palletsprojects.com/) for creating the command line
|
||||
interface. This allows for some new features like nested commands, setting
|
||||
parameters via environment variables, and shell completion. Backward
|
||||
compatibility should be mostly preserved, except for cases noted below.
|
||||
Please report any issues.
|
||||
changes:
|
||||
- "BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead"
|
||||
- "BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after `toot` but before the command"
|
||||
- "Enable passing params via environment variables, see: https://toot.bezdomni.net/environment_variables.html"
|
||||
- "BREAKING: Options `--debug` and `--color` must be specified after `toot` but before the command"
|
||||
- "BREAKING: Option `--quiet` has been removed. Redirect output instead."
|
||||
- "Add passing parameters via environment variables, see: https://toot.bezdomni.net/environment_variables.html"
|
||||
- "Add shell completion, see: https://toot.bezdomni.net/shell_completion.html"
|
||||
- "Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands"
|
||||
- "Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow`"
|
||||
- "Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands."
|
||||
- "Add `--json` option to tags commands"
|
||||
- "Add `--json` option to lists commands"
|
||||
- "Add `--json` option to tags and lists commands"
|
||||
- "Add `toot --width` option for setting your prefered terminal width"
|
||||
- "Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings."
|
||||
- "TUI: Fix issue where UI did not render until first input (thanks Urwid devs)"
|
||||
|
||||
0.39.0:
|
||||
date: 2023-11-23
|
||||
|
@ -3,20 +3,42 @@ Changelog
|
||||
|
||||
<!-- Do not edit. This file is automatically generated from changelog.yaml.-->
|
||||
|
||||
**0.40.0 (TBA)**
|
||||
**0.41.1 (2024-01-02)**
|
||||
|
||||
This release includes a major rewrite to use
|
||||
[Click](https://click.palletsprojects.com/) for creating the command line
|
||||
interface. This allows for some new features like nested commands, setting
|
||||
parameters via environment variables, and shell completion. See docs for
|
||||
details. Backward compatibility should be mostly preserved, except for cases
|
||||
noted below please report any issues.
|
||||
* Fix a crash in settings parsing code
|
||||
|
||||
**0.41.0 (2024-01-02)**
|
||||
|
||||
* Honour user's default visibility set in Mastodon preferences instead of always
|
||||
defaulting to public visibility (thanks Lexi Winter)
|
||||
* TUI: Add editing toots (thanks Lexi Winter)
|
||||
* TUI: Fix a bug which made pallette config in settings not work
|
||||
* TUI: Show edit datetime in status detail (thanks Lexi Winter)
|
||||
|
||||
**0.40.2 (2023-12-28)**
|
||||
|
||||
* Reinstate `toot post --using` option.
|
||||
* Add shell completion for instances.
|
||||
|
||||
**0.40.1 (2023-12-28)**
|
||||
|
||||
* Add `toot --as` option to replace `toot post --using`. This now works for all
|
||||
commands.
|
||||
|
||||
**0.40.0 (2023-12-27)**
|
||||
|
||||
This release includes a rather extensive change to use the Click library
|
||||
(https://click.palletsprojects.com/) for creating the command line interface.
|
||||
This allows for some new features like nested commands, setting parameters via
|
||||
environment variables, and shell completion. Backward compatibility should be
|
||||
mostly preserved, except for cases noted below. Please report any issues.
|
||||
|
||||
* BREAKING: Remove deprecated `--disable-https` option for `login` and
|
||||
`login_cli`, pass the base URL instead
|
||||
* BREAKING: Options `--debug`, `--color`, `--quiet` must be specified after
|
||||
`toot` but before the command
|
||||
* Enable passing params via environment variables, see:
|
||||
* BREAKING: Options `--debug` and `--color` must be specified after `toot` but
|
||||
before the command
|
||||
* BREAKING: Option `--quiet` has been removed. Redirect output instead.
|
||||
* Add passing parameters via environment variables, see:
|
||||
https://toot.bezdomni.net/environment_variables.html
|
||||
* Add shell completion, see: https://toot.bezdomni.net/shell_completion.html
|
||||
* Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature`
|
||||
@ -26,11 +48,11 @@ noted below please report any issues.
|
||||
* Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists
|
||||
list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`,
|
||||
`lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands.
|
||||
* Add `--json` option to tags commands
|
||||
* Add `--json` option to lists commands
|
||||
* Add `--json` option to tags and lists commands
|
||||
* Add `toot --width` option for setting your prefered terminal width
|
||||
* Add `--media-viewer` and `--colors` options to `toot tui`. These were
|
||||
previously accessible only via settings.
|
||||
* TUI: Fix issue where UI did not render until first input (thanks Urwid devs)
|
||||
|
||||
**0.39.0 (2023-11-23)**
|
||||
|
||||
|
@ -43,6 +43,7 @@ if dist_version != version:
|
||||
sys.exit(1)
|
||||
|
||||
release_date = changelog_item["date"]
|
||||
description = changelog_item.get("description")
|
||||
changes = changelog_item["changes"]
|
||||
|
||||
if not isinstance(release_date, date):
|
||||
@ -50,6 +51,11 @@ if not isinstance(release_date, date):
|
||||
sys.exit(1)
|
||||
|
||||
commit_message = f"toot {version}\n\n"
|
||||
|
||||
if description:
|
||||
lines = textwrap.wrap(description.strip(), 72)
|
||||
commit_message += "\n".join(lines) + "\n\n"
|
||||
|
||||
for c in changes:
|
||||
lines = textwrap.wrap(c, 70)
|
||||
initial = True
|
||||
|
2
setup.py
2
setup.py
@ -12,7 +12,7 @@ and blocking accounts and other actions.
|
||||
|
||||
setup(
|
||||
name='toot',
|
||||
version='0.40.0',
|
||||
version='0.41.1',
|
||||
description='Mastodon CLI client',
|
||||
long_description=long_description.strip(),
|
||||
author='Ivan Habunek',
|
||||
|
@ -9,15 +9,14 @@ your test server and database:
|
||||
|
||||
```
|
||||
export TOOT_TEST_BASE_URL="localhost:3000"
|
||||
export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
|
||||
```
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import psycopg2
|
||||
import pytest
|
||||
import re
|
||||
import typing as t
|
||||
import uuid
|
||||
|
||||
from click.testing import CliRunner, Result
|
||||
@ -31,8 +30,10 @@ def pytest_configure(config):
|
||||
toot.settings.DISABLE_SETTINGS = True
|
||||
|
||||
|
||||
# Type alias for run commands
|
||||
Run = t.Callable[..., Result]
|
||||
|
||||
# Mastodon database name, used to confirm user registration without having to click the link
|
||||
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
||||
TOOT_TEST_BASE_URL = os.getenv("TOOT_TEST_BASE_URL")
|
||||
|
||||
# Toot logo used for testing image upload
|
||||
@ -52,17 +53,9 @@ def register_account(app: App):
|
||||
email = f"{username}@example.com"
|
||||
|
||||
response = api.register_account(app, username, email, "password", "en")
|
||||
confirm_user(email)
|
||||
return User(app.instance, username, response["access_token"])
|
||||
|
||||
|
||||
def confirm_user(email):
|
||||
conn = psycopg2.connect(DATABASE_DSN)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE users SET confirmed_at = now() WHERE email = %s;", (email,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ------------------------------------------------------------------------------
|
||||
|
@ -3,7 +3,7 @@ from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from toot import User, cli
|
||||
from toot.cli import Run
|
||||
from tests.integration.conftest import Run
|
||||
|
||||
# TODO: figure out how to test login
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from tests.utils import run_with_retries
|
||||
from toot import api, cli
|
||||
from toot.exceptions import NotFoundError
|
||||
|
||||
@ -46,11 +46,11 @@ def test_favourite(app, user, run):
|
||||
assert result.exit_code == 0
|
||||
assert result.stdout.strip() == "✓ Status unfavourited"
|
||||
|
||||
# A short delay is required before the server returns new data
|
||||
time.sleep(0.2)
|
||||
|
||||
status = api.fetch_status(app, user, status["id"]).json()
|
||||
assert not status["favourited"]
|
||||
def test_favourited():
|
||||
nonlocal status
|
||||
status = api.fetch_status(app, user, status["id"]).json()
|
||||
assert not status["favourited"]
|
||||
run_with_retries(test_favourited)
|
||||
|
||||
|
||||
def test_favourite_json(app, user, run):
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from time import sleep
|
||||
from uuid import uuid4
|
||||
from tests.utils import run_with_retries
|
||||
|
||||
from toot import api, cli
|
||||
from toot.entities import from_dict, Status
|
||||
@ -40,16 +40,14 @@ def test_timelines(app, user, other_user, friend_user, friend_list, run):
|
||||
status2 = _post_status(app, other_user, "#bar")
|
||||
status3 = _post_status(app, friend_user, "#foo #bar")
|
||||
|
||||
# Give mastodon time to process things :/
|
||||
# Tests fail if this is removed, required delay depends on server speed
|
||||
sleep(1)
|
||||
|
||||
# Home timeline
|
||||
result = run(cli.timelines.timeline)
|
||||
assert result.exit_code == 0
|
||||
assert status1.id in result.stdout
|
||||
assert status2.id not in result.stdout
|
||||
assert status3.id in result.stdout
|
||||
def test_home():
|
||||
result = run(cli.timelines.timeline)
|
||||
assert result.exit_code == 0
|
||||
assert status1.id in result.stdout
|
||||
assert status2.id not in result.stdout
|
||||
assert status3.id in result.stdout
|
||||
run_with_retries(test_home)
|
||||
|
||||
# Public timeline
|
||||
result = run(cli.timelines.timeline, "--public")
|
||||
@ -166,13 +164,14 @@ def test_notifications(app, user, other_user, run):
|
||||
|
||||
text = f"Paging doctor @{user.username}"
|
||||
status = _post_status(app, other_user, text)
|
||||
sleep(0.5) # grr
|
||||
|
||||
result = run(cli.timelines.notifications)
|
||||
assert result.exit_code == 0
|
||||
assert f"@{other_user.username} mentioned you" in result.stdout
|
||||
assert status.id in result.stdout
|
||||
assert text in result.stdout
|
||||
def test_notifications():
|
||||
result = run(cli.timelines.notifications)
|
||||
assert result.exit_code == 0
|
||||
assert f"@{other_user.username} mentioned you" in result.stdout
|
||||
assert status.id in result.stdout
|
||||
assert text in result.stdout
|
||||
run_with_retries(test_notifications)
|
||||
|
||||
result = run(cli.timelines.notifications, "--mentions")
|
||||
assert result.exit_code == 0
|
||||
@ -186,7 +185,6 @@ def test_notifications_follow(app, user, friend_user, run_as):
|
||||
assert result.exit_code == 0
|
||||
assert f"@{user.username} now follows you" in result.stdout
|
||||
|
||||
|
||||
result = run_as(friend_user, cli.timelines.notifications, "--mentions")
|
||||
assert result.exit_code == 0
|
||||
assert "now follows you" not in result.stdout
|
||||
|
@ -2,6 +2,9 @@
|
||||
Helpers for testing.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class MockResponse:
|
||||
def __init__(self, response_data={}, ok=True, is_redirect=False):
|
||||
@ -19,3 +22,23 @@ class MockResponse:
|
||||
|
||||
def retval(val):
|
||||
return lambda *args, **kwargs: val
|
||||
|
||||
|
||||
def run_with_retries(fn: Callable[..., Any]):
|
||||
"""
|
||||
Run the the given function repeatedly until it finishes without raising an
|
||||
AssertionError. Sleep a bit between attempts. If the function doesn't
|
||||
succeed in the given number of tries raises the AssertionError. Used for
|
||||
tests which should eventually succeed.
|
||||
"""
|
||||
|
||||
# Wait upto 6 seconds with incrementally longer sleeps
|
||||
delays = [0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
|
||||
|
||||
for delay in delays:
|
||||
try:
|
||||
return fn()
|
||||
except AssertionError:
|
||||
time.sleep(delay)
|
||||
|
||||
fn()
|
||||
|
@ -4,7 +4,7 @@ import sys
|
||||
from os.path import join, expanduser
|
||||
from typing import NamedTuple
|
||||
|
||||
__version__ = '0.40.0'
|
||||
__version__ = '0.41.1'
|
||||
|
||||
|
||||
class App(NamedTuple):
|
||||
|
61
toot/api.py
61
toot/api.py
@ -183,7 +183,7 @@ def post_status(
|
||||
app,
|
||||
user,
|
||||
status,
|
||||
visibility='public',
|
||||
visibility=None,
|
||||
media_ids=None,
|
||||
sensitive=False,
|
||||
spoiler_text=None,
|
||||
@ -230,6 +230,52 @@ def post_status(
|
||||
return http.post(app, user, '/api/v1/statuses', json=data, headers=headers)
|
||||
|
||||
|
||||
def edit_status(
|
||||
app,
|
||||
user,
|
||||
id,
|
||||
status,
|
||||
visibility='public',
|
||||
media_ids=None,
|
||||
sensitive=False,
|
||||
spoiler_text=None,
|
||||
in_reply_to_id=None,
|
||||
language=None,
|
||||
content_type=None,
|
||||
poll_options=None,
|
||||
poll_expires_in=None,
|
||||
poll_multiple=None,
|
||||
poll_hide_totals=None,
|
||||
) -> Response:
|
||||
"""
|
||||
Edit an existing status
|
||||
https://docs.joinmastodon.org/methods/statuses/#edit
|
||||
"""
|
||||
|
||||
# Strip keys for which value is None
|
||||
# Sending null values doesn't bother Mastodon, but it breaks Pleroma
|
||||
data = drop_empty_values({
|
||||
'status': status,
|
||||
'media_ids': media_ids,
|
||||
'visibility': visibility,
|
||||
'sensitive': sensitive,
|
||||
'in_reply_to_id': in_reply_to_id,
|
||||
'language': language,
|
||||
'content_type': content_type,
|
||||
'spoiler_text': spoiler_text,
|
||||
})
|
||||
|
||||
if poll_options:
|
||||
data["poll"] = {
|
||||
"options": poll_options,
|
||||
"expires_in": poll_expires_in,
|
||||
"multiple": poll_multiple,
|
||||
"hide_totals": poll_hide_totals,
|
||||
}
|
||||
|
||||
return http.put(app, user, f"/api/v1/statuses/{id}", json=data)
|
||||
|
||||
|
||||
def fetch_status(app, user, id):
|
||||
"""
|
||||
Fetch a single status
|
||||
@ -238,6 +284,15 @@ def fetch_status(app, user, id):
|
||||
return http.get(app, user, f"/api/v1/statuses/{id}")
|
||||
|
||||
|
||||
def fetch_status_source(app, user, id):
|
||||
"""
|
||||
Fetch the source (original text) for a single status.
|
||||
This only works on local toots.
|
||||
https://docs.joinmastodon.org/methods/statuses/#source
|
||||
"""
|
||||
return http.get(app, user, f"/api/v1/statuses/{id}/source")
|
||||
|
||||
|
||||
def scheduled_statuses(app, user):
|
||||
"""
|
||||
List scheduled statuses
|
||||
@ -618,6 +673,10 @@ def get_instance(base_url: str) -> Response:
|
||||
return http.anon_get(url)
|
||||
|
||||
|
||||
def get_preferences(app, user) -> Response:
|
||||
return http.get(app, user, '/api/v1/preferences')
|
||||
|
||||
|
||||
def get_lists(app, user):
|
||||
return http.get(app, user, "/api/v1/lists").json()
|
||||
|
||||
|
@ -4,9 +4,12 @@ import os
|
||||
import sys
|
||||
import typing as t
|
||||
|
||||
from click.testing import Result
|
||||
from click.shell_completion import CompletionItem
|
||||
from click.types import StringParamType
|
||||
from functools import wraps
|
||||
|
||||
from toot import App, User, config, __version__
|
||||
from toot.output import print_warning
|
||||
from toot.settings import get_settings
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
@ -35,10 +38,6 @@ DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30
|
||||
seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\""""
|
||||
|
||||
|
||||
# Type alias for run commands
|
||||
Run = t.Callable[..., Result]
|
||||
|
||||
|
||||
def get_default_visibility() -> str:
|
||||
return os.getenv("TOOT_POST_VISIBILITY", "public")
|
||||
|
||||
@ -47,6 +46,17 @@ def get_default_map():
|
||||
settings = get_settings()
|
||||
common = settings.get("common", {})
|
||||
commands = settings.get("commands", {})
|
||||
|
||||
# TODO: remove in version 1.0
|
||||
tui_old = settings.get("tui", {}).copy()
|
||||
if "palette" in tui_old:
|
||||
del tui_old["palette"]
|
||||
if tui_old:
|
||||
# TODO: don't show the warning for [toot.palette]
|
||||
print_warning("Settings section [tui] has been deprecated in favour of [commands.tui].")
|
||||
tui_new = commands.get("tui", {})
|
||||
commands["tui"] = {**tui_old, **tui_new}
|
||||
|
||||
return {**common, **commands}
|
||||
|
||||
|
||||
@ -69,18 +79,44 @@ class Context(t.NamedTuple):
|
||||
user: t.Optional[User] = None
|
||||
color: bool = False
|
||||
debug: bool = False
|
||||
quiet: bool = False
|
||||
|
||||
|
||||
class TootObj(t.NamedTuple):
|
||||
"""Data to add to Click context"""
|
||||
color: bool = True
|
||||
debug: bool = False
|
||||
quiet: bool = False
|
||||
as_user: t.Optional[str] = None
|
||||
# Pass a context for testing purposes
|
||||
test_ctx: t.Optional[Context] = None
|
||||
|
||||
|
||||
class AccountParamType(StringParamType):
|
||||
"""Custom type to add shell completion for account names"""
|
||||
name = "account"
|
||||
|
||||
def shell_complete(self, ctx, param, incomplete: str):
|
||||
users = config.load_config()["users"].keys()
|
||||
return [
|
||||
CompletionItem(u)
|
||||
for u in users
|
||||
if u.lower().startswith(incomplete.lower())
|
||||
]
|
||||
|
||||
|
||||
class InstanceParamType(StringParamType):
|
||||
"""Custom type to add shell completion for instance domains"""
|
||||
name = "instance"
|
||||
|
||||
def shell_complete(self, ctx, param, incomplete: str):
|
||||
apps = config.load_config()["apps"]
|
||||
|
||||
return [
|
||||
CompletionItem(i)
|
||||
for i in apps.keys()
|
||||
if i.lower().startswith(incomplete.lower())
|
||||
]
|
||||
|
||||
|
||||
def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]":
|
||||
"""Pass the toot Context as first argument."""
|
||||
@wraps(f)
|
||||
@ -98,11 +134,16 @@ def get_context() -> Context:
|
||||
if obj.test_ctx:
|
||||
return obj.test_ctx
|
||||
|
||||
user, app = config.get_active_user_app()
|
||||
if not user or not app:
|
||||
raise click.ClickException("This command requires you to be logged in.")
|
||||
if obj.as_user:
|
||||
user, app = config.get_user_app(obj.as_user)
|
||||
if not user or not app:
|
||||
raise click.ClickException(f"Account '{obj.as_user}' not found. Run `toot auth` to see available accounts.")
|
||||
else:
|
||||
user, app = config.get_active_user_app()
|
||||
if not user or not app:
|
||||
raise click.ClickException("This command requires you to be logged in.")
|
||||
|
||||
return Context(app, user, obj.color, obj.debug, obj.quiet)
|
||||
return Context(app, user, obj.color, obj.debug)
|
||||
|
||||
|
||||
json_option = click.option(
|
||||
@ -117,12 +158,12 @@ json_option = click.option(
|
||||
@click.option("-w", "--max-width", type=int, default=80, help="Maximum width for content rendered by toot")
|
||||
@click.option("--debug/--no-debug", default=False, help="Log debug info to stderr")
|
||||
@click.option("--color/--no-color", default=sys.stdout.isatty(), help="Use ANSI color in output")
|
||||
@click.option("--quiet/--no-quiet", default=False, help="Don't print anything to stdout")
|
||||
@click.option("--as", "as_user", type=AccountParamType(), help="The account to use, overrides the active account.")
|
||||
@click.version_option(__version__, message="%(prog)s v%(version)s")
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, quiet: bool):
|
||||
def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, as_user: str):
|
||||
"""Toot is a Mastodon CLI"""
|
||||
ctx.obj = TootObj(color, debug, quiet)
|
||||
ctx.obj = TootObj(color, debug, as_user)
|
||||
ctx.color = color
|
||||
ctx.max_content_width = max_width
|
||||
|
||||
|
@ -2,13 +2,10 @@ import click
|
||||
import platform
|
||||
import sys
|
||||
import webbrowser
|
||||
from click.shell_completion import CompletionItem
|
||||
|
||||
from click.types import StringParamType
|
||||
|
||||
from toot import api, config, __version__
|
||||
from toot.auth import get_or_create_app, login_auth_code, login_username_password
|
||||
from toot.cli import cli
|
||||
from toot.cli import AccountParamType, cli
|
||||
from toot.cli.validators import validate_instance
|
||||
|
||||
|
||||
@ -22,18 +19,6 @@ instance_option = click.option(
|
||||
)
|
||||
|
||||
|
||||
class AccountParamType(StringParamType):
|
||||
"""Custom type to add shell completion for account names"""
|
||||
|
||||
def shell_complete(self, ctx, param, incomplete: str):
|
||||
accounts = config.load_config()["users"].keys()
|
||||
return [
|
||||
CompletionItem(a)
|
||||
for a in accounts
|
||||
if a.lower().startswith(incomplete.lower())
|
||||
]
|
||||
|
||||
|
||||
@cli.command()
|
||||
def auth():
|
||||
"""Show logged in accounts and instances"""
|
||||
|
@ -11,7 +11,8 @@ from toot.output import print_list_accounts, print_lists, print_warning
|
||||
def lists(ctx: click.Context):
|
||||
"""Display and manage lists"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
print_warning("`toot lists` is deprecated in favour of `toot lists list`")
|
||||
print_warning("`toot lists` is deprecated in favour of `toot lists list`.\n" +
|
||||
"Run `toot lists -h` to see other list-related commands.")
|
||||
|
||||
user, app = config.get_active_user_app()
|
||||
if not user or not app:
|
||||
|
@ -6,8 +6,8 @@ from datetime import datetime, timedelta, timezone
|
||||
from time import sleep, time
|
||||
from typing import BinaryIO, Optional, Tuple
|
||||
|
||||
from toot import api
|
||||
from toot.cli import cli, json_option, pass_context, Context
|
||||
from toot import api, config
|
||||
from toot.cli import AccountParamType, cli, json_option, pass_context, Context
|
||||
from toot.cli import DURATION_EXAMPLES, VISIBILITY_CHOICES
|
||||
from toot.cli.validators import validate_duration, validate_language
|
||||
from toot.entities import MediaAttachment, from_dict
|
||||
@ -40,7 +40,6 @@ from toot.utils.datetime import parse_datetime
|
||||
"--visibility", "-v",
|
||||
help="Post visibility",
|
||||
type=click.Choice(VISIBILITY_CHOICES),
|
||||
default="public",
|
||||
)
|
||||
@click.option(
|
||||
"--sensitive", "-s",
|
||||
@ -106,6 +105,11 @@ from toot.utils.datetime import parse_datetime
|
||||
is_flag=True,
|
||||
default=False,
|
||||
)
|
||||
@click.option(
|
||||
"-u", "--using",
|
||||
type=AccountParamType(),
|
||||
help="The account to use, overrides the active account.",
|
||||
)
|
||||
@json_option
|
||||
@pass_context
|
||||
def post(
|
||||
@ -114,7 +118,7 @@ def post(
|
||||
media: Tuple[str],
|
||||
descriptions: Tuple[str],
|
||||
thumbnails: Tuple[str],
|
||||
visibility: str,
|
||||
visibility: Optional[str],
|
||||
sensitive: bool,
|
||||
spoiler_text: Optional[str],
|
||||
reply_to: Optional[str],
|
||||
@ -127,12 +131,20 @@ def post(
|
||||
poll_expires_in: int,
|
||||
poll_multiple: bool,
|
||||
poll_hide_totals: bool,
|
||||
json: bool
|
||||
json: bool,
|
||||
using: str
|
||||
):
|
||||
"""Post a new status"""
|
||||
if len(media) > 4:
|
||||
raise click.ClickException("Cannot attach more than 4 files.")
|
||||
|
||||
if using:
|
||||
user, app = config.get_user_app(using)
|
||||
if not user or not app:
|
||||
raise click.ClickException(f"Account '{using}' not found. Run `toot auth` to see available accounts.")
|
||||
else:
|
||||
user, app = ctx.user, ctx.app
|
||||
|
||||
media_ids = _upload_media(ctx.app, ctx.user, media, descriptions, thumbnails)
|
||||
status_text = _get_status_text(text, editor, media)
|
||||
scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in)
|
||||
@ -141,8 +153,8 @@ def post(
|
||||
raise click.ClickException("You must specify either text or media to post.")
|
||||
|
||||
response = api.post_status(
|
||||
ctx.app,
|
||||
ctx.user,
|
||||
app,
|
||||
user,
|
||||
status_text,
|
||||
visibility=visibility,
|
||||
media_ids=media_ids,
|
||||
|
@ -9,7 +9,7 @@ from toot.cli.validators import validate_instance
|
||||
from toot.entities import Instance, Status, from_dict, Account
|
||||
from toot.exceptions import ApiError, ConsoleError
|
||||
from toot.output import print_account, print_instance, print_search_results, print_status, print_timeline
|
||||
from toot.cli import cli, get_context, json_option, pass_context, Context
|
||||
from toot.cli import InstanceParamType, cli, get_context, json_option, pass_context, Context
|
||||
|
||||
|
||||
@cli.command()
|
||||
@ -43,7 +43,7 @@ def whois(ctx: Context, account: str, json: bool):
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("instance", callback=validate_instance, required=False)
|
||||
@click.argument("instance", type=InstanceParamType(), callback=validate_instance, required=False)
|
||||
@json_option
|
||||
def instance(instance: Optional[str], json: bool):
|
||||
"""Display instance details
|
||||
|
@ -2,7 +2,7 @@ import sys
|
||||
import click
|
||||
|
||||
from toot import api
|
||||
from toot.cli import cli, get_context, pass_context, Context
|
||||
from toot.cli import InstanceParamType, cli, get_context, pass_context, Context
|
||||
from typing import Optional
|
||||
from toot.cli.validators import validate_instance
|
||||
|
||||
@ -13,6 +13,7 @@ from toot.output import print_notifications, print_timeline
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--instance", "-i",
|
||||
type=InstanceParamType(),
|
||||
callback=validate_instance,
|
||||
help="""Domain or base URL of the instance from which to read,
|
||||
e.g. 'mastodon.social' or 'https://mastodon.social'""",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import click
|
||||
|
||||
from typing import Optional
|
||||
from toot.cli import TUI_COLORS, Context, cli, pass_context
|
||||
from toot.cli import TUI_COLORS, VISIBILITY_CHOICES, Context, cli, pass_context
|
||||
from toot.cli.validators import validate_tui_colors, validate_cache_size
|
||||
from toot.tui.app import TUI, TuiOptions
|
||||
|
||||
@ -30,13 +30,25 @@ COLOR_OPTIONS = ", ".join(TUI_COLORS.keys())
|
||||
help="""Specify the image cache maximum size in megabytes. Default: 10MB.
|
||||
Minimum: 1MB."""
|
||||
)
|
||||
@click.option(
|
||||
"-v", "--default-visibility",
|
||||
type=click.Choice(VISIBILITY_CHOICES),
|
||||
help="Default visibility when posting new toots; overrides the server-side preference"
|
||||
)
|
||||
@click.option(
|
||||
"-S", "--always-show-sensitive",
|
||||
is_flag=True,
|
||||
help="Expand toots with content warnings automatically"
|
||||
)
|
||||
@pass_context
|
||||
def tui(
|
||||
ctx: Context,
|
||||
colors: Optional[int],
|
||||
media_viewer: Optional[str],
|
||||
always_show_sensitive: bool,
|
||||
relative_datetimes: bool,
|
||||
cache_size: Optional[int],
|
||||
default_visibility: Optional[str]
|
||||
):
|
||||
"""Launches the toot terminal user interface"""
|
||||
if colors is None:
|
||||
@ -47,6 +59,8 @@ def tui(
|
||||
media_viewer=media_viewer,
|
||||
relative_datetimes=relative_datetimes,
|
||||
cache_size=cache_size,
|
||||
default_visibility=default_visibility,
|
||||
always_show_sensitive=always_show_sensitive,
|
||||
)
|
||||
tui = TUI.create(ctx.app, ctx.user, options)
|
||||
tui.run()
|
||||
|
18
toot/http.py
18
toot/http.py
@ -38,7 +38,7 @@ def _get_error_message(response):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "Unknown error"
|
||||
return f"Unknown error: {response.status_code} {response.reason}"
|
||||
|
||||
|
||||
def process_response(response):
|
||||
@ -81,6 +81,22 @@ def post(app, user, path, headers=None, files=None, data=None, json=None, allow_
|
||||
return anon_post(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)
|
||||
|
||||
|
||||
def anon_put(url, headers=None, files=None, data=None, json=None, allow_redirects=True):
|
||||
request = Request(method="PUT", url=url, headers=headers, files=files, data=data, json=json)
|
||||
response = send_request(request, allow_redirects)
|
||||
|
||||
return process_response(response)
|
||||
|
||||
|
||||
def put(app, user, path, headers=None, files=None, data=None, json=None, allow_redirects=True):
|
||||
url = app.base_url + path
|
||||
|
||||
headers = headers or {}
|
||||
headers["Authorization"] = f"Bearer {user.access_token}"
|
||||
|
||||
return anon_put(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects)
|
||||
|
||||
|
||||
def patch(app, user, path, headers=None, files=None, data=None, json=None):
|
||||
url = app.base_url + path
|
||||
|
||||
|
@ -274,8 +274,9 @@ def print_notification(notification: Notification):
|
||||
|
||||
def print_notifications(notifications: List[Notification]):
|
||||
for notification in notifications:
|
||||
print_divider()
|
||||
print_notification(notification)
|
||||
if notification.type not in ['pleroma:emoji_reaction']:
|
||||
print_divider()
|
||||
print_notification(notification)
|
||||
print_divider()
|
||||
|
||||
|
||||
|
@ -6,11 +6,13 @@ import warnings
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import NamedTuple, Optional
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from toot import api, config, __version__, settings
|
||||
from toot import App, User
|
||||
from toot.cli import get_default_visibility
|
||||
from toot.exceptions import ApiError
|
||||
from toot.utils.datetime import parse_datetime
|
||||
|
||||
from .compose import StatusComposer
|
||||
from .constants import PALETTE
|
||||
@ -22,7 +24,8 @@ from .poll import Poll
|
||||
from .timeline import Timeline
|
||||
from .utils import get_max_toot_chars, parse_content_links, copy_to_clipboard, ImageCache
|
||||
from PIL import Image
|
||||
|
||||
from .widgets import ModalBox
|
||||
>>>>>>> master
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -35,9 +38,10 @@ DEFAULT_MAX_TOOT_CHARS = 500
|
||||
class TuiOptions(NamedTuple):
|
||||
colors: int
|
||||
media_viewer: Optional[str]
|
||||
always_show_sensitive: bool
|
||||
relative_datetimes: bool
|
||||
cache_size: int
|
||||
|
||||
default_visibility: Optional[bool]
|
||||
|
||||
class Header(urwid.WidgetWrap):
|
||||
def __init__(self, app, user):
|
||||
@ -143,6 +147,7 @@ class TUI(urwid.Frame):
|
||||
self.can_translate = False
|
||||
self.account = None
|
||||
self.followed_accounts = []
|
||||
self.preferences = {}
|
||||
|
||||
if self.options.cache_size:
|
||||
self.cache_max = 1024 * 1024 * self.options.cache_size
|
||||
@ -153,6 +158,7 @@ class TUI(urwid.Frame):
|
||||
|
||||
def run(self):
|
||||
self.loop.set_alarm_in(0, lambda *args: self.async_load_instance())
|
||||
self.loop.set_alarm_in(0, lambda *args: self.async_load_preferences())
|
||||
self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline(
|
||||
is_initial=True, timeline_name="home"))
|
||||
self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_accounts())
|
||||
@ -337,6 +343,19 @@ class TUI(urwid.Frame):
|
||||
|
||||
return self.run_in_thread(_load_instance, done_callback=_done)
|
||||
|
||||
def async_load_preferences(self):
|
||||
"""
|
||||
Attempt to update user preferences from instance.
|
||||
https://docs.joinmastodon.org/methods/preferences/
|
||||
"""
|
||||
def _load_preferences():
|
||||
return api.get_preferences(self.app, self.user).json()
|
||||
|
||||
def _done(preferences):
|
||||
self.preferences = preferences
|
||||
|
||||
return self.run_in_thread(_load_preferences, done_callback=_done)
|
||||
|
||||
def async_load_followed_accounts(self):
|
||||
def _load_accounts():
|
||||
try:
|
||||
@ -411,11 +430,45 @@ class TUI(urwid.Frame):
|
||||
def _post(timeline, *args):
|
||||
self.post_status(*args)
|
||||
|
||||
composer = StatusComposer(self.max_toot_chars, self.user.username, in_reply_to)
|
||||
# If the user specified --default-visibility, use that; otherwise,
|
||||
# try to use the server-side default visibility. If that fails, fall
|
||||
# back to get_default_visibility().
|
||||
visibility = (self.options.default_visibility or
|
||||
self.preferences.get('posting:default:visibility',
|
||||
get_default_visibility()))
|
||||
|
||||
composer = StatusComposer(self.max_toot_chars, self.user.username,
|
||||
visibility, in_reply_to)
|
||||
urwid.connect_signal(composer, "close", _close)
|
||||
urwid.connect_signal(composer, "post", _post)
|
||||
self.open_overlay(composer, title="Compose status")
|
||||
|
||||
def async_edit(self, status):
|
||||
def _fetch_source():
|
||||
return api.fetch_status_source(self.app, self.user, status.id).json()
|
||||
|
||||
def _done(source):
|
||||
self.close_overlay()
|
||||
self.show_edit(status, source)
|
||||
|
||||
please_wait = ModalBox("Loading status...")
|
||||
self.open_overlay(please_wait)
|
||||
|
||||
self.run_in_thread(_fetch_source, done_callback=_done)
|
||||
|
||||
def show_edit(self, status, source):
|
||||
def _close(*args):
|
||||
self.close_overlay()
|
||||
|
||||
def _edit(timeline, *args):
|
||||
self.edit_status(status, *args)
|
||||
|
||||
composer = StatusComposer(self.max_toot_chars, self.user.username,
|
||||
visibility=None, edit=status, source=source)
|
||||
urwid.connect_signal(composer, "close", _close)
|
||||
urwid.connect_signal(composer, "post", _edit)
|
||||
self.open_overlay(composer, title="Edit status")
|
||||
|
||||
def show_goto_menu(self):
|
||||
user_timelines = self.config.get("timelines", {})
|
||||
user_lists = api.get_lists(self.app, self.user) or []
|
||||
@ -563,6 +616,42 @@ class TUI(urwid.Frame):
|
||||
self.footer.set_message("Status posted {} \\o/".format(status.id))
|
||||
self.close_overlay()
|
||||
|
||||
def edit_status(self, status, content, warning, visibility, in_reply_to_id):
|
||||
# We don't support editing polls (yet), so to avoid losing the poll
|
||||
# data from the original toot, copy it to the edit request.
|
||||
poll_args = {}
|
||||
poll = status.original.data.get('poll', None)
|
||||
|
||||
if poll is not None:
|
||||
poll_args['poll_options'] = [o['title'] for o in poll['options']]
|
||||
poll_args['poll_multiple'] = poll['multiple']
|
||||
|
||||
# Convert absolute expiry time into seconds from now.
|
||||
expires_at = parse_datetime(poll['expires_at'])
|
||||
expires_in = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
||||
poll_args['poll_expires_in'] = expires_in
|
||||
|
||||
if 'hide_totals' in poll:
|
||||
poll_args['poll_hide_totals'] = poll['hide_totals']
|
||||
|
||||
data = api.edit_status(
|
||||
self.app,
|
||||
self.user,
|
||||
status.id,
|
||||
content,
|
||||
spoiler_text=warning,
|
||||
visibility=visibility,
|
||||
**poll_args
|
||||
).json()
|
||||
|
||||
new_status = self.make_status(data)
|
||||
|
||||
self.footer.set_message("Status edited {} \\o/".format(status.id))
|
||||
self.close_overlay()
|
||||
|
||||
if self.timeline is not None:
|
||||
self.timeline.update_status(new_status)
|
||||
|
||||
def show_account(self, account_id):
|
||||
account = api.whois(self.app, self.user, account_id)
|
||||
relationship = api.get_relationship(self.app, self.user, account_id)
|
||||
|
@ -1,8 +1,6 @@
|
||||
import urwid
|
||||
import logging
|
||||
|
||||
from toot.cli import get_default_visibility
|
||||
|
||||
from .constants import VISIBILITY_OPTIONS
|
||||
from .widgets import Button, EditBox
|
||||
|
||||
@ -11,21 +9,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class StatusComposer(urwid.Frame):
|
||||
"""
|
||||
UI for compose and posting a status message.
|
||||
UI for composing or editing a status message.
|
||||
|
||||
To edit a status, provide the original status in 'edit', and optionally
|
||||
provide the status source (from the /status/:id/source API endpoint) in
|
||||
'source'; this should have at least a 'text' member, and optionally
|
||||
'spoiler_text'. If source is not provided, the formatted HTML will be
|
||||
presented to the user for editing.
|
||||
"""
|
||||
signals = ["close", "post"]
|
||||
|
||||
def __init__(self, max_chars, username, in_reply_to=None):
|
||||
def __init__(self, max_chars, username, visibility, in_reply_to=None,
|
||||
edit=None, source=None):
|
||||
self.in_reply_to = in_reply_to
|
||||
self.max_chars = max_chars
|
||||
self.username = username
|
||||
|
||||
text = self.get_initial_text(in_reply_to)
|
||||
self.content_edit = EditBox(
|
||||
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
|
||||
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)
|
||||
|
||||
self.char_count = urwid.Text(["0/{}".format(max_chars)])
|
||||
self.edit = edit
|
||||
|
||||
self.cw_edit = None
|
||||
self.cw_add_button = Button("Add content warning",
|
||||
@ -33,13 +32,34 @@ class StatusComposer(urwid.Frame):
|
||||
self.cw_remove_button = Button("Remove content warning",
|
||||
on_press=self.remove_content_warning)
|
||||
|
||||
self.visibility = (
|
||||
in_reply_to.visibility if in_reply_to else get_default_visibility()
|
||||
)
|
||||
if edit:
|
||||
if source is None:
|
||||
text = edit.data["content"]
|
||||
else:
|
||||
text = source.get("text", edit.data["content"])
|
||||
|
||||
if 'spoiler_text' in source:
|
||||
self.cw_edit = EditBox(multiline=True, allow_tab=True,
|
||||
edit_text=source['spoiler_text'])
|
||||
|
||||
self.visibility = edit.data["visibility"]
|
||||
|
||||
else: # not edit
|
||||
text = self.get_initial_text(in_reply_to)
|
||||
self.visibility = (
|
||||
in_reply_to.visibility if in_reply_to else visibility
|
||||
)
|
||||
|
||||
self.content_edit = EditBox(
|
||||
edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True)
|
||||
urwid.connect_signal(self.content_edit.edit, "change", self.text_changed)
|
||||
|
||||
self.char_count = urwid.Text(["0/{}".format(max_chars)])
|
||||
|
||||
self.visibility_button = Button("Visibility: {}".format(self.visibility),
|
||||
on_press=self.choose_visibility)
|
||||
|
||||
self.post_button = Button("Post", on_press=self.post)
|
||||
self.post_button = Button("Edit" if edit else "Post", on_press=self.post)
|
||||
self.cancel_button = Button("Cancel", on_press=self.close)
|
||||
|
||||
contents = list(self.generate_list_items())
|
||||
|
@ -53,6 +53,10 @@ class Status:
|
||||
self.id = self.data["id"]
|
||||
self.account = self._get_account()
|
||||
self.created_at = parse_datetime(data["created_at"])
|
||||
if data["edited_at"]:
|
||||
self.edited_at = parse_datetime(data["edited_at"])
|
||||
else:
|
||||
self.edited_at = None
|
||||
self.author = self._get_author()
|
||||
self.favourited = data.get("favourited", False)
|
||||
self.reblogged = data.get("reblogged", False)
|
||||
|
@ -231,6 +231,7 @@ class Help(urwid.Padding):
|
||||
yield urwid.Text(h(" [N] - Translate status if possible (toggle)"))
|
||||
yield urwid.Text(h(" [R] - Reply to current status"))
|
||||
yield urwid.Text(h(" [S] - Show text marked as sensitive"))
|
||||
yield urwid.Text(h(" [M] - Show status media"))
|
||||
yield urwid.Text(h(" [T] - Show status thread (replies)"))
|
||||
yield urwid.Text(h(" [L] - Show the status links"))
|
||||
yield urwid.Text(h(" [U] - Show the status data in JSON as received from the server"))
|
||||
|
@ -109,6 +109,7 @@ class Timeline(urwid.Columns):
|
||||
"[A]ccount" if not status.is_mine else "",
|
||||
"[B]oost",
|
||||
"[D]elete" if status.is_mine else "",
|
||||
"[E]dit" if status.is_mine else "",
|
||||
"B[o]okmark",
|
||||
"[F]avourite",
|
||||
"[V]iew",
|
||||
@ -198,6 +199,11 @@ class Timeline(urwid.Columns):
|
||||
self.tui.show_delete_confirmation(status)
|
||||
return
|
||||
|
||||
if key in ("e", "E"):
|
||||
if status.is_mine:
|
||||
self.tui.async_edit(status)
|
||||
return
|
||||
|
||||
if key in ("f", "F"):
|
||||
self.tui.async_toggle_favourite(self, status)
|
||||
return
|
||||
@ -349,6 +355,7 @@ class StatusDetails(urwid.Pile):
|
||||
if self.status:
|
||||
self.status.placeholders = []
|
||||
self.followed_accounts = timeline.tui.followed_accounts
|
||||
self.options = timeline.tui.options
|
||||
|
||||
reblogged_by = status.author if status and status.reblog else None
|
||||
widget_list = list(self.content_generator(status.original, reblogged_by)
|
||||
@ -447,9 +454,12 @@ class StatusDetails(urwid.Pile):
|
||||
yield ("pack", urwid.Divider())
|
||||
|
||||
# Show content warning
|
||||
if status.data["spoiler_text"] and not status.show_sensitive:
|
||||
if status.data["spoiler_text"] and not status.show_sensitive and not self.options.always_show_sensitive:
|
||||
yield ("pack", urwid.Text(("content_warning", "Marked as sensitive. Press S to view.")))
|
||||
else:
|
||||
if status.data["spoiler_text"]:
|
||||
yield ("pack", urwid.Text(("content_warning", "Marked as sensitive.")))
|
||||
|
||||
content = status.original.translation if status.original.show_translation else status.data["content"]
|
||||
widgetlist = html_to_widgets(content)
|
||||
|
||||
@ -516,6 +526,8 @@ class StatusDetails(urwid.Pile):
|
||||
|
||||
yield ("pack", urwid.Text([
|
||||
("status_detail_timestamp", f"{status.created_at.strftime('%Y-%m-%d %H:%M')} "),
|
||||
("status_detail_timestamp",
|
||||
f"(edited {status.edited_at.strftime('%Y-%m-%d %H:%M')}) " if status.edited_at else ""),
|
||||
("status_detail_bookmarked" if status.bookmarked else "dim", "b "),
|
||||
("dim", f"⤶ {status.data['replies_count']} "),
|
||||
("highlight" if status.reblogged else "dim", f"♺ {status.data['reblogs_count']} "),
|
||||
@ -579,7 +591,7 @@ class StatusDetails(urwid.Pile):
|
||||
|
||||
class StatusListItem(SelectableColumns):
|
||||
def __init__(self, status, relative_datetimes):
|
||||
edited_at = status.data.get("edited_at")
|
||||
edited_at = status.original.edited_at
|
||||
|
||||
# TODO: hacky implementation to avoid creating conflicts for existing
|
||||
# pull reuqests, refactor when merged.
|
||||
@ -593,7 +605,7 @@ class StatusListItem(SelectableColumns):
|
||||
favourited = ("highlight", "★") if status.original.favourited else " "
|
||||
reblogged = ("highlight", "♺") if status.original.reblogged else " "
|
||||
is_reblog = ("dim", "♺") if status.reblog else " "
|
||||
is_reply = ("dim", "⤶") if status.original.in_reply_to else " "
|
||||
is_reply = ("dim", "⤶ ") if status.original.in_reply_to else " "
|
||||
|
||||
return super().__init__([
|
||||
("pack", SelectableText(("status_list_timestamp", created_at), wrap="clip")),
|
||||
|
@ -159,3 +159,10 @@ class EmojiText(urwid.Padding):
|
||||
columns.append(("weight", 9999, urwid.Text("")))
|
||||
column_widget = urwid.Columns(columns, dividechars=1, min_width=2)
|
||||
super().__init__(column_widget)
|
||||
|
||||
class ModalBox(urwid.Frame):
|
||||
def __init__(self, message):
|
||||
text = urwid.Text(message)
|
||||
filler = urwid.Filler(text, valign='top', top=1, bottom=1)
|
||||
padding = urwid.Padding(filler, left=1, right=1)
|
||||
return super().__init__(padding)
|
||||
|
Loading…
Reference in New Issue
Block a user