Create polls (#1452)

* add AddPollDialog

* add support for pleroma poll options

* add PollPreviewView

* add Poll support to drafts

* add license header, cleanup

* rename drawable files to correct size

* fix tests

* fix bug with Poll having wrong duration after delete&redraft

* add input validation

* grey out poll button when its disabled

* code cleanup & small improvements
This commit is contained in:
Konrad Pozniak 2019-08-22 20:30:08 +02:00 committed by GitHub
parent 444df322a7
commit 51c6852492
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1540 additions and 76 deletions

View file

@ -0,0 +1,711 @@
{
"formatVersion": 1,
"database": {
"version": 19,
"identityHash": "84ebd39cba4d6749251d330851b70e36",
"entities": [
{
"tableName": "TootEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "descriptions",
"columnName": "descriptions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToText",
"columnName": "inReplyToText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToUsername",
"columnName": "inReplyToUsername",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorServerId",
"columnName": "authorServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToAccountId",
"columnName": "inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogsCount",
"columnName": "reblogsCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favouritesCount",
"columnName": "favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accounts",
"columnName": "accounts",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.id",
"columnName": "s_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.url",
"columnName": "s_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToId",
"columnName": "s_inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToAccountId",
"columnName": "s_inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.account",
"columnName": "s_account",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.content",
"columnName": "s_content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.createdAt",
"columnName": "s_createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.emojis",
"columnName": "s_emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.favouritesCount",
"columnName": "s_favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsible",
"columnName": "s_collapsible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84ebd39cba4d6749251d330851b70e36')"
]
}
}

View file

@ -79,6 +79,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Instance;
import com.keylesspalace.tusky.entity.NewPoll;
import com.keylesspalace.tusky.entity.SearchResults;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi;
@ -94,9 +95,11 @@ import com.keylesspalace.tusky.util.SaveTootHelper;
import com.keylesspalace.tusky.util.SpanUtilsKt;
import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.AddPollDialog;
import com.keylesspalace.tusky.view.ComposeOptionsListener;
import com.keylesspalace.tusky.view.ComposeOptionsView;
import com.keylesspalace.tusky.view.EditTextTyped;
import com.keylesspalace.tusky.view.PollPreviewView;
import com.keylesspalace.tusky.view.ProgressImageView;
import com.keylesspalace.tusky.view.TootButton;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
@ -190,6 +193,7 @@ public final class ComposeActivity
private static final String REPLYING_STATUS_CONTENT_EXTRA = "replying_status_content";
private static final String MEDIA_ATTACHMENTS_EXTRA = "media_attachments";
private static final String SENSITIVE_EXTRA = "sensitive";
private static final String POLL_EXTRA = "poll";
// Mastodon only counts URLs as this long in terms of status character limits
static final int MAXIMUM_URL_LENGTH = 23;
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
@ -213,6 +217,7 @@ public final class ComposeActivity
private ImageButton contentWarningButton;
private ImageButton emojiButton;
private ImageButton hideMediaToggle;
private TextView actionAddPoll;
private Button atButton;
private Button hashButton;
@ -222,11 +227,14 @@ public final class ComposeActivity
private BottomSheetBehavior emojiBehavior;
private RecyclerView emojiView;
private PollPreviewView pollPreview;
// this only exists when a status is trying to be sent, but uploads are still occurring
private ProgressDialog finishingUploadDialog;
private String inReplyToId;
private List<QueuedMedia> mediaQueued = new ArrayList<>();
private CountUpDownLatch waitForMediaLatch;
private NewPoll poll;
private Status.Visibility statusVisibility; // The current values of the options that will be applied
private boolean statusMarkSensitive; // to the status being composed.
private boolean statusHideText;
@ -239,6 +247,8 @@ public final class ComposeActivity
private List<Emoji> emojiList;
private CountDownLatch emojiListRetrievalLatch = new CountDownLatch(1);
private int maximumTootCharacters = STATUS_CHARACTER_LIMIT;
private Integer maxPollOptions = null;
private Integer maxPollOptionLength = null;
private @Px
int thumbnailViewSize;
@ -369,6 +379,7 @@ public final class ComposeActivity
TextView actionPhotoTake = findViewById(R.id.action_photo_take);
TextView actionPhotoPick = findViewById(R.id.action_photo_pick);
actionAddPoll = findViewById(R.id.action_add_poll);
int textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary);
@ -378,8 +389,12 @@ public final class ComposeActivity
Drawable imageIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).color(textColor).sizeDp(18);
actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null);
Drawable pollIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).color(textColor).sizeDp(18);
actionAddPoll.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null);
actionPhotoTake.setOnClickListener(v -> initiateCameraApp());
actionPhotoPick.setOnClickListener(v -> onMediaPick());
actionAddPoll.setOnClickListener(v -> openPollDialog());
thumbnailViewSize = getResources().getDimensionPixelSize(R.dimen.compose_media_preview_size);
@ -507,6 +522,14 @@ public final class ComposeActivity
}
statusMarkSensitive = intent.getBooleanExtra(SENSITIVE_EXTRA, statusMarkSensitive);
if(intent.hasExtra(POLL_EXTRA) && (mediaAttachments == null || mediaAttachments.size() == 0)) {
updatePoll(intent.getParcelableExtra(POLL_EXTRA));
}
if(mediaAttachments != null && mediaAttachments.size() > 0) {
enablePollButton(false);
}
}
// After the starting state is finalised, the interface can be set to reflect this state.
@ -901,6 +924,62 @@ public final class ComposeActivity
addMediaBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
private void openPollDialog() {
addMediaBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
AddPollDialog.showAddPollDialog(this, poll, maxPollOptions, maxPollOptionLength);
}
public void updatePoll(NewPoll poll) {
this.poll = poll;
enableButton(pickButton, false, false);
if(pollPreview == null) {
pollPreview = new PollPreviewView(this);
Resources resources = getResources();
int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin);
int marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(margin, margin, margin, marginBottom);
pollPreview.setLayoutParams(layoutParams);
mediaPreviewBar.addView(pollPreview);
pollPreview.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(this, pollPreview);
final int editId = 1;
final int removeId = 2;
popup.getMenu().add(0, editId, 0, R.string.edit_poll);
popup.getMenu().add(0, removeId, 0, R.string.action_remove);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case editId:
openPollDialog();
break;
case removeId:
removePoll();
break;
}
return true;
});
popup.show();
});
}
pollPreview.setPoll(poll);
}
private void removePoll() {
poll = null;
pollPreview = null;
enableButton(pickButton, true, true);
mediaPreviewBar.removeAllViews();
}
@Override
public void onVisibilityChanged(@NonNull Status.Visibility visibility) {
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
@ -1005,7 +1084,7 @@ public final class ComposeActivity
}
Intent sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
visibility, !mediaUris.isEmpty() && sensitive, mediaIds, mediaUris, mediaDescriptions, inReplyToId,
visibility, !mediaUris.isEmpty() && sensitive, mediaIds, mediaUris, mediaDescriptions, inReplyToId, poll,
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
@ -1162,6 +1241,18 @@ public final class ComposeActivity
colorActive ? android.R.attr.textColorTertiary : R.attr.compose_media_button_disabled_tint);
}
private void enablePollButton(boolean enable) {
actionAddPoll.setEnabled(enable);
int textColor;
if(enable) {
textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary);
} else {
textColor = ThemeUtils.getColor(this, R.attr.compose_media_button_disabled_tint);
}
actionAddPoll.setTextColor(textColor);
actionAddPoll.getCompoundDrawablesRelative()[0].setColorFilter(textColor, PorterDuff.Mode.SRC_IN);
}
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize, @Nullable String description) {
addMediaToQueue(null, type, preview, uri, mediaSize, null, description);
}
@ -1210,6 +1301,7 @@ public final class ComposeActivity
}
updateHideMediaToggle();
enablePollButton(false);
if (item.readyStage != QueuedMedia.ReadyStage.UPLOADED) {
waitForMediaLatch.countUp();
@ -1259,7 +1351,7 @@ public final class ComposeActivity
final int addCaptionId = 1;
final int removeId = 2;
popup.getMenu().add(0, addCaptionId, 0, R.string.action_set_caption);
popup.getMenu().add(0, removeId, 0, R.string.action_remove_media);
popup.getMenu().add(0, removeId, 0, R.string.action_remove);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case addCaptionId:
@ -1378,6 +1470,7 @@ public final class ComposeActivity
mediaQueued.remove(item);
if (mediaQueued.size() == 0) {
updateHideMediaToggle();
enablePollButton(true);
}
updateContentDescriptionForAllImages();
enableButton(pickButton, true, true);
@ -1685,8 +1778,9 @@ public final class ComposeActivity
boolean contentWarningChanged = contentWarningBar.getVisibility() == View.VISIBLE &&
!TextUtils.isEmpty(contentWarning) && !startingContentWarning.startsWith(contentWarning.toString());
boolean mediaChanged = !mediaQueued.isEmpty();
boolean pollChanged = poll != null;
if (textChanged || contentWarningChanged || mediaChanged) {
if (textChanged || contentWarningChanged || mediaChanged || pollChanged) {
new AlertDialog.Builder(this)
.setMessage(R.string.compose_save_draft)
.setPositiveButton(R.string.action_save, (d, w) -> saveDraftAndFinish())
@ -1722,7 +1816,8 @@ public final class ComposeActivity
inReplyToId,
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
statusVisibility);
statusVisibility,
poll);
finishWithoutSlideOutAnimation();
}
@ -1808,6 +1903,8 @@ public final class ComposeActivity
if (instanceEntity != null) {
Integer max = instanceEntity.getMaximumTootCharacters();
maximumTootCharacters = (max == null ? STATUS_CHARACTER_LIMIT : max);
maxPollOptions = instanceEntity.getMaxPollOptions();
maxPollOptionLength = instanceEntity.getMaxPollOptionLength();
setEmojiList(instanceEntity.getEmojiList());
updateVisibleCharactersLeft();
}
@ -1825,7 +1922,9 @@ public final class ComposeActivity
}
private void cacheInstanceMetadata(@NotNull AccountEntity activeAccount) {
InstanceEntity instanceEntity = new InstanceEntity(activeAccount.getDomain(), emojiList, maximumTootCharacters);
InstanceEntity instanceEntity = new InstanceEntity(
activeAccount.getDomain(), emojiList, maximumTootCharacters, maxPollOptions, maxPollOptionLength
);
database.instanceDao().insertOrReplace(instanceEntity);
}
@ -1840,9 +1939,18 @@ public final class ComposeActivity
}
private void onFetchInstanceSuccess(Instance instance) {
if (instance != null && instance.getMaxTootChars() != null) {
maximumTootCharacters = instance.getMaxTootChars();
updateVisibleCharactersLeft();
if (instance != null) {
if (instance.getMaxTootChars() != null) {
maximumTootCharacters = instance.getMaxTootChars();
updateVisibleCharactersLeft();
}
if (instance.getPollLimits() != null) {
maxPollOptions = instance.getPollLimits().getMaxOptions();
maxPollOptionLength = instance.getPollLimits().getMaxOptionChars();
}
cacheInstanceMetadata(accountManager.getActiveAccount());
}
}
@ -1966,7 +2074,8 @@ public final class ComposeActivity
private ArrayList<Attachment> mediaAttachments;
@Nullable
private Boolean sensitive;
@Nullable
private NewPoll poll;
public IntentBuilder savedTootUid(int uid) {
this.savedTootUid = uid;
@ -2033,6 +2142,11 @@ public final class ComposeActivity
return this;
}
public IntentBuilder poll(NewPoll poll) {
this.poll = poll;
return this;
}
public Intent build(Context context) {
Intent intent = new Intent(context, ComposeActivity.class);
@ -2073,9 +2187,12 @@ public final class ComposeActivity
if (mediaAttachments != null) {
intent.putParcelableArrayListExtra(MEDIA_ATTACHMENTS_EXTRA, mediaAttachments);
}
if(sensitive != null) {
if (sensitive != null) {
intent.putExtra(SENSITIVE_EXTRA, sensitive);
}
if (poll != null) {
intent.putExtra(POLL_EXTRA, poll);
}
return intent;
}
}

View file

@ -163,6 +163,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
.replyingStatusAuthor(item.getInReplyToUsername())
.replyingStatusContent(item.getInReplyToText())
.visibility(item.getVisibility())
.poll(item.getPoll())
.build(this);
startActivity(intent);
}

View file

@ -68,7 +68,7 @@ public class TuskyApplication extends Application implements HasAndroidInjector
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18)
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19)
.build();
accountManager = new AccountManager(appDatabase);
serviceLocator = new ServiceLocator() {

View file

@ -0,0 +1,92 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.visible
class AddPollOptionsAdapter(
private var options: MutableList<String>,
private val maxOptionLength: Int,
private val onOptionRemoved: () -> Unit,
private val onOptionChanged: (Boolean) -> Unit
): RecyclerView.Adapter<ViewHolder>() {
val pollOptions: List<String>
get() = options.toList()
fun addChoice() {
options.add("")
notifyItemInserted(options.size - 1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val holder = ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_add_poll_option, parent, false))
holder.editText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
holder.editText.onTextChanged { s, _, _, _ ->
val pos = holder.adapterPosition
if(pos != RecyclerView.NO_POSITION) {
options[pos] = s.toString()
onOptionChanged(validateInput())
}
}
return holder
}
override fun getItemCount() = options.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.editText.setText(options[position])
holder.textInputLayout.hint = holder.textInputLayout.context.getString(R.string.poll_new_choice_hint, position + 1)
holder.deleteButton.visible(position > 1, View.INVISIBLE)
holder.deleteButton.setOnClickListener {
holder.editText.clearFocus()
options.removeAt(holder.adapterPosition)
notifyItemRemoved(holder.adapterPosition)
onOptionRemoved()
}
}
private fun validateInput(): Boolean {
if (options.contains("") || options.distinct().size != options.size) {
return false
}
return true
}
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val textInputLayout: TextInputLayout = itemView.findViewById(R.id.optionTextInputLayout)
val editText: TextInputEditText = itemView.findViewById(R.id.optionEditText)
val deleteButton: ImageButton = itemView.findViewById(R.id.deleteButton)
}

View file

@ -0,0 +1,70 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.ThemeUtils
class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
private var options: List<String> = emptyList()
private var multiple: Boolean = false
private var clickListener: View.OnClickListener? = null
fun update(newOptions: List<String>, multiple: Boolean) {
this.options = newOptions
this.multiple = multiple
notifyDataSetChanged()
}
fun setOnClickListener(l: View.OnClickListener?) {
clickListener = l
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
return PreviewViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll_preview_option, parent, false))
}
override fun getItemCount() = options.size
override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) {
val textView = holder.itemView as TextView
val iconId = if (multiple) {
R.drawable.ic_check_box_outline_blank_18dp
} else {
R.drawable.ic_radio_button_unchecked_18dp
}
val iconDrawable = ThemeUtils.getTintedDrawable(textView.context, iconId, android.R.attr.textColorTertiary)
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, iconDrawable, null, null, null)
textView.text = options[position]
textView.setOnClickListener(clickListener)
}
}
class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)

View file

@ -398,6 +398,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
.contentWarning(status.spoilerText)
.mediaAttachments(status.attachments)
.sensitive(status.sensitive)
.poll(status.poll?.toNewPoll(status.createdAt))
.build(context)
startActivity(intent)
}

View file

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 18)
}, version = 19)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
@ -300,4 +300,14 @@ public abstract class AppDatabase extends RoomDatabase {
}
};
public static final Migration MIGRATION_18_19 = new Migration(18, 19) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptions` INTEGER");
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptionLength` INTEGER");
database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `poll` TEXT");
}
};
}

View file

@ -25,4 +25,7 @@ import com.keylesspalace.tusky.entity.Emoji
data class InstanceEntity(
@field:PrimaryKey var instance: String,
val emojiList: List<Emoji>?,
val maximumTootCharacters: Int?)
val maximumTootCharacters: Int?,
val maxPollOptions: Int?,
val maxPollOptionLength: Int?
)

View file

@ -15,6 +15,8 @@
package com.keylesspalace.tusky.db;
import com.google.gson.Gson;
import com.keylesspalace.tusky.entity.NewPoll;
import com.keylesspalace.tusky.entity.Status;
import androidx.annotation.Nullable;
@ -60,9 +62,13 @@ public class TootEntity {
@ColumnInfo(name = "visibility")
private final Status.Visibility visibility;
@Nullable
@ColumnInfo(name = "poll")
private final NewPoll poll;
public TootEntity(int uid, String text, String urls, String descriptions, String contentWarning, String inReplyToId,
@Nullable String inReplyToText, @Nullable String inReplyToUsername,
Status.Visibility visibility) {
Status.Visibility visibility, @Nullable NewPoll poll) {
this.uid = uid;
this.text = text;
this.urls = urls;
@ -72,6 +78,7 @@ public class TootEntity {
this.inReplyToText = inReplyToText;
this.inReplyToUsername = inReplyToUsername;
this.visibility = visibility;
this.poll = poll;
}
public String getText() {
@ -112,8 +119,15 @@ public class TootEntity {
return visibility;
}
@Nullable
public NewPoll getPoll() {
return poll;
}
public static final class Converters {
private static final Gson gson = new Gson();
@TypeConverter
public Status.Visibility visibilityFromInt(int number) {
return Status.Visibility.byNum(number);
@ -123,5 +137,15 @@ public class TootEntity {
public int intFromVisibility(Status.Visibility visibility) {
return visibility == null ? Status.Visibility.UNKNOWN.getNum() : visibility.getNum();
}
@TypeConverter
public String pollToString(NewPoll poll) {
return gson.toJson(poll);
}
@TypeConverter
public NewPoll stringToPoll(String poll) {
return gson.fromJson(poll, NewPoll.class);
}
}
}

View file

@ -29,7 +29,8 @@ data class Instance (
val languages: List<String>,
@SerializedName("contact_account") val contactAccount: Account,
@SerializedName("max_toot_chars") val maxTootChars: Int?,
@SerializedName("max_bio_chars") val maxBioChars: Int?
@SerializedName("max_bio_chars") val maxBioChars: Int?,
@SerializedName("poll_limits") val pollLimits: PollLimits?
) {
override fun hashCode(): Int {
return uri.hashCode()
@ -44,3 +45,7 @@ data class Instance (
}
}
data class PollLimits (
@SerializedName("max_options") val maxOptions: Int?,
@SerializedName("max_option_chars") val maxOptionChars: Int?
)

View file

@ -0,0 +1,37 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.entity
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize
data class NewStatus(
val status: String,
@SerializedName("spoiler_text") val warningText: String,
@SerializedName("in_reply_to_id") val inReplyToId: String?,
val visibility: String,
val sensitive: Boolean,
@SerializedName("media_ids") val mediaIds: List<String>?,
val poll: NewPoll?
)
@Parcelize
data class NewPoll(
val options: List<String>,
@SerializedName("expires_in") val expiresIn: Int,
val multiple: Boolean
): Parcelable

View file

@ -25,6 +25,14 @@ data class Poll(
return copy(options = newOptions, votesCount = votesCount + choices.size, voted = true)
}
fun toNewPoll(creationDate: Date) = NewPoll(
options.map { it.title },
expiresAt?.let {
((it.time - creationDate.time) / 1000).toInt() + 1
}?: 3600,
multiple
)
}
data class PollOption(

View file

@ -364,14 +364,18 @@ public abstract class SFragment extends BaseFragment implements Injectable {
timelineCases.delete(id);
removeItem(position);
Intent intent = new ComposeActivity.IntentBuilder()
ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder()
.tootText(getEditableText(status.getContent(), status.getMentions()))
.inReplyToId(status.getInReplyToId())
.visibility(status.getVisibility())
.contentWarning(status.getSpoilerText())
.mediaAttachments(status.getAttachments())
.sensitive(status.getSensitive())
.build(getContext());
.sensitive(status.getSensitive());
if(status.getPoll() != null) {
intentBuilder.poll(status.getPoll().toNewPoll(status.getCreatedAt()));
}
Intent intent = intentBuilder.build(getContext());
startActivity(intent);
})
.setNegativeButton(android.R.string.cancel, null)
@ -470,7 +474,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
boolean shouldFilterStatus(Status status) {
return (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find()
|| (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find())));
|| (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find())));
}
private void applyFilters(boolean refresh) {

View file

@ -24,6 +24,7 @@ import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Instance;
import com.keylesspalace.tusky.entity.MastoList;
import com.keylesspalace.tusky.entity.NewStatus;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Relationship;
@ -43,6 +44,7 @@ import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
@ -126,18 +128,12 @@ public interface MastodonApi {
Call<Attachment> updateMedia(@Path("mediaId") String mediaId,
@Field("description") String description);
@FormUrlEncoded
@POST("api/v1/statuses")
Call<Status> createStatus(
@Header("Authorization") String auth,
@Header(DOMAIN_HEADER) String domain,
@Field("status") String text,
@Field("in_reply_to_id") String inReplyToId,
@Field("spoiler_text") String warningText,
@Field("visibility") String visibility,
@Field("sensitive") Boolean sensitive,
@Field("media_ids[]") List<String> mediaIds,
@Header("Idempotency-Key") String idempotencyKey);
@Header("Idempotency-Key") String idempotencyKey,
@Body NewStatus status);
@GET("api/v1/statuses/{id}")
Call<Status> status(@Path("id") String statusId);

View file

@ -95,6 +95,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
citedStatusId,
null,
null,
null,
null, account, 0)
context.startService(sendIntent)

View file

@ -22,6 +22,8 @@ import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.NewStatus
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.SaveTootHelper
@ -131,16 +133,21 @@ class SendTootService : Service(), Injectable {
tootToSend.retries++
val sendCall = mastodonApi.createStatus(
"Bearer " + account.accessToken,
account.domain,
val newStatus = NewStatus(
tootToSend.text,
tootToSend.inReplyToId,
tootToSend.warningText,
tootToSend.inReplyToId,
tootToSend.visibility,
tootToSend.sensitive,
tootToSend.mediaIds,
tootToSend.idempotencyKey
tootToSend.poll
)
val sendCall = mastodonApi.createStatus(
"Bearer " + account.accessToken,
account.domain,
tootToSend.idempotencyKey,
newStatus
)
@ -243,7 +250,8 @@ class SendTootService : Service(), Injectable {
toot.inReplyToId,
toot.replyingStatusContent,
toot.replyingStatusAuthorUsername,
Status.Visibility.byString(toot.visibility))
Status.Visibility.byString(toot.visibility),
toot.poll)
}
private fun cancelSendingIntent(tootId: Int): PendingIntent {
@ -277,6 +285,7 @@ class SendTootService : Service(), Injectable {
mediaUris: List<Uri>,
mediaDescriptions: List<String>,
inReplyToId: String?,
poll: NewPoll?,
replyingStatusContent: String?,
replyingStatusAuthorUsername: String?,
savedJsonUrls: String?,
@ -295,6 +304,7 @@ class SendTootService : Service(), Injectable {
mediaUris.map { it.toString() },
mediaDescriptions,
inReplyToId,
poll,
replyingStatusContent,
replyingStatusAuthorUsername,
savedJsonUrls,
@ -337,6 +347,7 @@ data class TootToSend(val text: String,
val mediaUris: List<String>,
val mediaDescriptions: List<String>,
val inReplyToId: String?,
val poll: NewPoll?,
val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?,
val savedJsonUrls: String?,

View file

@ -17,6 +17,7 @@ import com.google.gson.reflect.TypeToken;
import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.entity.NewPoll;
import com.keylesspalace.tusky.entity.Status;
import java.io.File;
@ -41,17 +42,18 @@ public final class SaveTootHelper {
@SuppressLint("StaticFieldLeak")
public boolean saveToot(@NonNull String content,
@NonNull String contentWarning,
@Nullable String savedJsonUrls,
@NonNull List<String> mediaUris,
@NonNull List<String> mediaDescriptions,
int savedTootUid,
@Nullable String inReplyToId,
@Nullable String replyingStatusContent,
@Nullable String replyingStatusAuthorUsername,
@NonNull Status.Visibility statusVisibility) {
@NonNull String contentWarning,
@Nullable String savedJsonUrls,
@NonNull List<String> mediaUris,
@NonNull List<String> mediaDescriptions,
int savedTootUid,
@Nullable String inReplyToId,
@Nullable String replyingStatusContent,
@Nullable String replyingStatusAuthorUsername,
@NonNull Status.Visibility statusVisibility,
@Nullable NewPoll poll) {
if (TextUtils.isEmpty(content) && mediaUris.isEmpty()) {
if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) {
return false;
}
@ -86,7 +88,8 @@ public final class SaveTootHelper {
inReplyToId,
replyingStatusContent,
replyingStatusAuthorUsername,
statusVisibility);
statusVisibility,
poll);
new AsyncTask<Void, Void, Void>() {
@Override

View file

@ -0,0 +1,102 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("AddPollDialog")
package com.keylesspalace.tusky.view
import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.ComposeActivity
import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter
import com.keylesspalace.tusky.entity.NewPoll
import kotlinx.android.synthetic.main.dialog_add_poll.view.*
import android.view.WindowManager
import com.keylesspalace.tusky.R
private const val DEFAULT_MAX_OPTION_COUNT = 4
private const val DEFAULT_MAX_OPTION_LENGTH = 25
fun showAddPollDialog(
activity: ComposeActivity,
poll: NewPoll?,
maxOptionCount: Int?,
maxOptionLength: Int?
) {
val view = activity.layoutInflater.inflate(R.layout.dialog_add_poll, null)
val dialog = AlertDialog.Builder(activity)
.setIcon(R.drawable.ic_poll_24dp)
.setTitle(R.string.create_poll_title)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, null)
.create()
val adapter = AddPollOptionsAdapter(
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
maxOptionLength = maxOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
onOptionRemoved = {
view.addChoiceButton.isEnabled = true
},
onOptionChanged = { valid ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
}
)
view.pollChoices.adapter = adapter
view.addChoiceButton.setOnClickListener {
if (adapter.itemCount < maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
adapter.addChoice()
}
if (adapter.itemCount >= maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
it.isEnabled = false
}
}
val pollDurationId = activity.resources.getIntArray(R.array.poll_duration_values).indexOfLast {
it <= poll?.expiresIn ?: 0
}
view.pollDurationSpinner.setSelection(pollDurationId)
view.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false
dialog.setOnShowListener {
val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
button.setOnClickListener {
val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition
val pollDuration = activity.resources.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
activity.updatePoll(
NewPoll(
options = adapter.pollOptions,
expiresIn = pollDuration,
multiple = view.multipleChoicesCheckBox.isChecked
)
)
dialog.dismiss()
}
}
dialog.show()
// make the dialog focusable so the keyboard does not stay behind it
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}

View file

@ -0,0 +1,64 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.view
import android.content.Context
import android.util.AttributeSet
import android.widget.LinearLayout
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter
import com.keylesspalace.tusky.entity.NewPoll
import kotlinx.android.synthetic.main.view_poll_preview.view.*
class PollPreviewView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
val adapter = PreviewPollOptionsAdapter()
init {
inflate(context, R.layout.view_poll_preview, this)
orientation = VERTICAL
setBackgroundResource(R.drawable.card_frame)
val padding = resources.getDimensionPixelSize(R.dimen.poll_preview_padding)
setPadding(padding, padding, padding, padding)
pollPreviewOptions.adapter = adapter
}
fun setPoll(poll: NewPoll){
adapter.update(poll.options, poll.multiple)
val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast {
it <= poll.expiresIn
}
pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId]
}
override fun setOnClickListener(l: OnClickListener?) {
super.setOnClickListener(l)
adapter.setOnClickListener(l)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View file

@ -190,6 +190,15 @@
android:padding="8dp"
android:text="@string/action_add_media"
android:textSize="?attr/status_text_medium" />
<TextView
android:id="@+id/action_add_poll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="8dp"
android:padding="8dp"
android:text="@string/action_add_poll"
android:textSize="?attr/status_text_medium" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pollChoices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/addChoiceButton"
style="@style/TuskyButton.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/add_poll_choice"
app:layout_constraintEnd_toStartOf="@id/pollDurationSpinner"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pollChoices" />
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/pollDurationSpinner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:entries="@array/poll_duration_names"
app:layout_constraintBottom_toBottomOf="@id/addChoiceButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/addChoiceButton"
app:layout_constraintTop_toTopOf="@id/addChoiceButton" />
<CheckBox
android:id="@+id/multipleChoicesCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/poll_allow_multiple_choices"
app:buttonTint="?attr/compound_button_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/addChoiceButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/optionTextInputLayout"
style="@style/TuskyTextInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_weight="1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/optionEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/deleteButton"
style="?attr/image_button_style"
android:layout_marginStart="8dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/action_remove"
android:layout_gravity="bottom"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_clear_24dp" />
</LinearLayout>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:ellipsize="end"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:lines="1"
android:maxEms="20" />

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:background="@drawable/card_frame"
tools:padding="@dimen/poll_preview_padding"
tools:parentTag="android.widget.LinearLayout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_poll_24dp"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:text="@string/create_poll_title"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pollPreviewOptions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<TextView
android:id="@+id/pollDurationPreview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="5 Minutes" />
</merge>

View file

@ -280,7 +280,7 @@
<string name="hint_describe_for_visually_impaired">وصف لضعاف البصر
\n(%d أحرف على أقصى تقدير)</string>
<string name="action_set_caption">إضافة شرح</string>
<string name="action_remove_media">حذف</string>
<string name="action_remove">حذف</string>
<string name="lock_account_label">تجميد الحساب</string>
<string name="lock_account_label_description">يتطلب منك قبول طلبات المتابَعة يدويا</string>
<string name="compose_save_draft">هل تود الإحتفاظ بالمسودة ؟</string>

View file

@ -334,7 +334,7 @@
<string name="hint_describe_for_visually_impaired">দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন
\n(%d অক্ষর সীমা)</string>
<string name="action_set_caption">ক্যাপশন সেট করুন</string>
<string name="action_remove_media">সরান</string>
<string name="action_remove">সরান</string>
<string name="lock_account_label">অ্যাকাউন্ট লক করুন</string>
<string name="lock_account_label_description">অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন</string>
<string name="compose_save_draft">ড্রাফট সংরক্ষণ\?</string>

View file

@ -347,7 +347,7 @@
<string name="hint_describe_for_visually_impaired">Descriure per a invidentes
\n(%d character limit)</string>
<string name="action_set_caption">Afegir una llegenda</string>
<string name="action_remove_media">Eliminar</string>
<string name="action_remove">Eliminar</string>
<string name="lock_account_label">Protegir el compte</string>
<string name="lock_account_label_description">S\'haurà d\'admetre els seguidors manualment</string>
<string name="compose_save_draft">Guardar l\'esborrany\?</string>

View file

@ -289,7 +289,7 @@
<string name="error_failed_set_caption">Nastavení popisku selhalo</string>
<string name="hint_describe_for_visually_impaired">Popis pro zrakově postižené\n(limit %d znaků)</string>
<string name="action_set_caption">Nastavit popisek</string>
<string name="action_remove_media">Odstranit</string>
<string name="action_remove">Odstranit</string>
<string name="lock_account_label">Uzamknout účet</string>
<string name="lock_account_label_description">Vyžaduje, abyste ručně schvaloval/a sledující</string>
<string name="compose_save_draft">Uložit koncept?</string>

View file

@ -239,7 +239,7 @@
<string name="compose_active_account_description">Yn postio â chyfrif %1$s</string>
<string name="error_failed_set_caption">Methu gosod pennawd</string>
<string name="action_set_caption">Pennu pennawd</string>
<string name="action_remove_media">Dileu</string>
<string name="action_remove">Dileu</string>
<string name="lock_account_label">Cloi cyfrif</string>
<string name="lock_account_label_description">Angen cymeradwyo dilynwyr eich hun</string>
<string name="compose_save_draft">Cadw drafft?</string>

View file

@ -268,7 +268,7 @@
<string name="error_failed_set_caption">Fehler beim Speichern der Beschreibung</string>
<string name="hint_describe_for_visually_impaired">Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen)</string>
<string name="action_set_caption">Beschreibung eingeben</string>
<string name="action_remove_media">entfernen</string>
<string name="action_remove">entfernen</string>
<string name="lock_account_label">Gesperrtes Profil</string>
<string name="lock_account_label_description">Wer dir folgen möchte, muss um deine Erlaubnis bitten</string>
<string name="compose_save_draft">Entwurf speichern?</string>

View file

@ -285,7 +285,7 @@
<string name="error_failed_set_caption">Redakto de apudskribo malsukcesis</string>
<string name="hint_describe_for_visually_impaired">Priskribi por misvidantaj homoj\n(%d signoj maksimume)</string>
<string name="action_set_caption">Redakti apudskribon</string>
<string name="action_remove_media">Forigi</string>
<string name="action_remove">Forigi</string>
<string name="lock_account_label">Ŝlosi konton</string>
<string name="lock_account_label_description">Vi devas permane rajtigi sekvantojn</string>
<string name="compose_save_draft">Konservi malneton?</string>

View file

@ -255,7 +255,7 @@
<string name="error_failed_set_caption">Error al añadir leyenda</string>
<string name="hint_describe_for_visually_impaired">Describir para invidentes\n(límite de %d caracteres)</string>
<string name="action_set_caption">Añadir leyenda</string>
<string name="action_remove_media">Eliminar</string>
<string name="action_remove">Eliminar</string>
<string name="lock_account_label">Proteger cuenta</string>
<string name="lock_account_label_description">Tendrá que admitir los seguidores manualmente</string>
<string name="compose_save_draft">¿Guardar borrador?</string>

View file

@ -237,7 +237,7 @@
<string name="error_failed_set_caption">Akatsa deskribapena eranstean</string>
<string name="hint_describe_for_visually_impaired">Ikusmen urritasuna dutenentzat deskribapena\n(%d karaktereko muga)</string>
<string name="action_set_caption">Deskribapena erantsi</string>
<string name="action_remove_media">Ezabatu</string>
<string name="action_remove">Ezabatu</string>
<string name="lock_account_label">Kontua babestu</string>
<string name="lock_account_label_description">Jarraitzaileak eskuz onartu beharko dituzu</string>
<string name="compose_save_draft">Zirriborroa gorde?</string>

View file

@ -231,7 +231,7 @@
<string name="error_failed_set_caption">ناتوان در تنظیم عنوان</string>
<string name="hint_describe_for_visually_impaired">توصیف برای کم‌بینایان\n(محدودیت نویسه %d)</string>
<string name="action_set_caption">تنظیم عنوان</string>
<string name="action_remove_media">حذف</string>
<string name="action_remove">حذف</string>
<string name="lock_account_label">قفل حساب</string>
<string name="lock_account_label_description">به شما امکان می‌دهد بصورت دستی دنبال‌کنندگان را تایید کنید</string>
<string name="compose_save_draft">ذخیره به عنوان پیش‌نویس</string>

View file

@ -290,7 +290,7 @@
<string name="hint_describe_for_visually_impaired">Décrire pour les malvoyants
\n(%d caractères maximum)</string>
<string name="action_set_caption">Mettre une légende</string>
<string name="action_remove_media">Supprimer le média</string>
<string name="action_remove">Supprimer le média</string>
<string name="lock_account_label">Verrouiller le compte</string>
<string name="lock_account_label_description">Vous devez approuvez manuellement les abonnements</string>
<string name="compose_save_draft">Enregistrer comme brouillon ?</string>

View file

@ -237,7 +237,7 @@
<string name="add_account_description">Új Mastodon fiók hozzáadása</string>
<string name="action_lists">Listák</string>
<string name="title_lists">Listák</string>
<string name="action_remove_media">Törlés</string>
<string name="action_remove">Törlés</string>
<string name="lock_account_label">Fiók lezárása</string>
<string name="compose_save_draft">Elmented a vázlatot?</string>
<string name="send_toot_notification_title">Tülk elküldése…</string>

View file

@ -283,7 +283,7 @@
<string name="error_failed_set_caption">Impostazione del sottotitolo non riuscita</string>
<string name="hint_describe_for_visually_impaired">Descrivi per ipovedenti\n(limite di %d caratteri)</string>
<string name="action_set_caption">Inserisci descrizione</string>
<string name="action_remove_media">Rimuovi</string>
<string name="action_remove">Rimuovi</string>
<string name="lock_account_label">Blocca account</string>
<string name="lock_account_label_description">Richiede la tua approvazione manuale di chi ti segue</string>
<string name="compose_save_draft">Salvare bozza?</string>

View file

@ -264,7 +264,7 @@
<string name="error_failed_set_caption">説明の設定に失敗しました</string>
<string name="hint_describe_for_visually_impaired">視覚障害者のための説明 (%d文字まで)</string>
<string name="action_set_caption">説明を設定</string>
<string name="action_remove_media">消去</string>
<string name="action_remove">消去</string>
<string name="lock_account_label">アカウントをロック</string>
<string name="lock_account_label_description">フォロワーを手動で承認する必要があります</string>
<string name="compose_save_draft">下書きを保存しますか?</string>

View file

@ -347,7 +347,7 @@
<string name="hint_describe_for_visually_impaired">시각 장애인을 위한 설명
\n(%d글자 작성 가능)</string>
<string name="action_set_caption">설명 추가</string>
<string name="action_remove_media">삭제</string>
<string name="action_remove">삭제</string>
<string name="lock_account_label">계정 잠금</string>
<string name="lock_account_label_description">팔로워를 수동으로 승인합니다</string>
<string name="compose_save_draft">작성 중인 내용을 저장하시겠습니까\?</string>

View file

@ -263,7 +263,7 @@
<string name="error_failed_set_caption">Toevoegen van beschrijving mislukt</string>
<string name="hint_describe_for_visually_impaired">Omschrijf dit voor mensen met een visuele beperking\n(tekenlimiet is %d)</string>
<string name="action_set_caption">Beschrijving toevoegen</string>
<string name="action_remove_media">Verwijderen</string>
<string name="action_remove">Verwijderen</string>
<string name="lock_account_label">Account besloten maken</string>
<string name="lock_account_label_description">Handmatige goedkeuring vereist voor volgers</string>
<string name="compose_save_draft">Concept bewaren?</string>

View file

@ -304,7 +304,7 @@
<string name="hint_describe_for_visually_impaired">Beskriv for de med nedsatt synsevne
\n(maks %d tegn)</string>
<string name="action_set_caption">Sett bildetekst</string>
<string name="action_remove_media">Slett</string>
<string name="action_remove">Slett</string>
<string name="lock_account_label">Lås konto</string>
<string name="lock_account_label_description">Krever at du manuelt godkjenner nye følgere</string>
<string name="compose_save_draft">Lagre kladd\?</string>

View file

@ -230,7 +230,7 @@
<string name="compose_active_account_description">Publicar amb lo compte %1$s</string>
<string name="error_failed_set_caption">Fracàs en apondre una legenda</string>
<string name="action_set_caption">Apondre una legenda</string>
<string name="action_remove_media">Levar</string>
<string name="action_remove">Levar</string>
<string name="lock_account_label">Clavar lo compte</string>
<string name="lock_account_label_description">Demanda que validetz manualament los seguidors</string>
<string name="compose_save_draft">Salvar lo borrolhon ?</string>

View file

@ -233,7 +233,7 @@
<string name="compose_active_account_description">Publikowanie z konta %1$s</string>
<string name="error_failed_set_caption">Nie udało się ustawić podpisu</string>
<string name="action_set_caption">Ustaw podpis</string>
<string name="action_remove_media">Usuń</string>
<string name="action_remove">Usuń</string>
<string name="lock_account_label">Zablokuj konto</string>
<string name="lock_account_label_description">Wymaga od Ciebie ręcznej akceptacji próśb o śledzenie</string>
<string name="compose_save_draft">Czy chcesz zapisać szkic?</string>

View file

@ -251,7 +251,7 @@
<string name="compose_active_account_description">Usando a conta %1$s</string>
<string name="error_failed_set_caption">Falha ao incluir descrição</string>
<string name="action_set_caption">Descrever</string>
<string name="action_remove_media">Remover</string>
<string name="action_remove">Remover</string>
<string name="lock_account_label">Trancar conta</string>
<string name="lock_account_label_description">Requer aprovação manual de seguidores</string>
<string name="compose_save_draft">Salvar rascunho?</string>

View file

@ -352,7 +352,7 @@
<string name="error_failed_set_caption">Не удалось добавить подпись</string>
<string name="hint_describe_for_visually_impaired">Описание для слабовидящих\n(не более %d символов)</string>
<string name="action_set_caption">Добавить подпись</string>
<string name="action_remove_media">Удалить</string>
<string name="action_remove">Удалить</string>
<string name="lock_account_label">Закрыть аккаунт</string>
<string name="lock_account_label_description">Вам придётся вручную подтверждать подписчиков</string>
<string name="compose_save_draft">Сохранить черновик?</string>

View file

@ -304,7 +304,7 @@
<string name="hint_describe_for_visually_impaired">Opišite za slabovidne
\n(omejitev znakov - %d)</string>
<string name="action_set_caption">Nastavi opis</string>
<string name="action_remove_media">Odstrani</string>
<string name="action_remove">Odstrani</string>
<string name="lock_account_label">Zakleni račun</string>
<string name="lock_account_label_description">Zahtevana je ročna potrditev sledilcev</string>
<string name="compose_save_draft">Shrani osnutek\?</string>

View file

@ -284,7 +284,7 @@
<string name="error_failed_set_caption">Misslyckades med att ange bildtext</string>
<string name="hint_describe_for_visually_impaired">Beskriv för synskadade\n(%d teckengräns)</string>
<string name="action_set_caption">Ange bildtext</string>
<string name="action_remove_media">Ta bort</string>
<string name="action_remove">Ta bort</string>
<string name="lock_account_label">Lås konto</string>
<string name="lock_account_label_description">Kräver att du manuellt godkänner följare</string>
<string name="compose_save_draft">Spara utkast?</string>

View file

@ -219,7 +219,7 @@
<string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string>
<string name="error_failed_set_caption">தலைப்பை அமைக்க முடியவில்லை</string>
<string name="action_set_caption">தலைப்பை அமை</string>
<string name="action_remove_media">நீக்கு</string>
<string name="action_remove">நீக்கு</string>
<string name="lock_account_label">கணக்கை முடக்கு</string>
<string name="lock_account_label_description">நீங்களாக பின்பற்றுபவர்களை அங்கீகரிக்க</string>
<string name="compose_save_draft">வரைவை சேமிக்கவா?</string>

View file

@ -250,7 +250,7 @@
<string name="hint_describe_for_visually_impaired">Görme engelliler için açıklama
\n(%d karakter limiti)</string>
<string name="action_set_caption">Başlık belirle</string>
<string name="action_remove_media">Kaldır</string>
<string name="action_remove">Kaldır</string>
<string name="lock_account_label">Hesabı Gizle</string>
<string name="lock_account_label_description">Aktif edilirse takipçileri elle onaylamanız gerekir</string>
<string name="compose_save_draft">Taslaklara kaydedilsin mi\?</string>

View file

@ -337,7 +337,7 @@
<string name="error_failed_set_caption">设置图片标题失败</string>
<string name="hint_describe_for_visually_impaired">为视觉障碍用户提供的描述\n(限制 %d 字)</string>
<string name="action_set_caption">设置图片标题</string>
<string name="action_remove_media">移除</string>
<string name="action_remove">移除</string>
<string name="lock_account_label">保护你的帐户(锁嘟)</string>
<string name="lock_account_label_description">你需要手动审核所有关注请求</string>
<string name="compose_save_draft">保存为草稿?</string>

View file

@ -332,7 +332,7 @@
<string name="error_failed_set_caption">設定圖片標題失敗</string>
<string name="hint_describe_for_visually_impaired">為視覺障礙用戶提供的描述\n(限制 %d 字)</string>
<string name="action_set_caption">設定圖片標題</string>
<string name="action_remove_media">移除</string>
<string name="action_remove">移除</string>
<string name="lock_account_label">保護你的帳戶(鎖嘟)</string>
<string name="lock_account_label_description">你需要手動審核所有關注請求</string>
<string name="compose_save_draft">儲存為草稿?</string>

View file

@ -332,7 +332,7 @@
<string name="error_failed_set_caption">設定圖片標題失敗</string>
<string name="hint_describe_for_visually_impaired">為視覺障礙用戶提供的描述\n(限制 %d 字)</string>
<string name="action_set_caption">設定圖片標題</string>
<string name="action_remove_media">移除</string>
<string name="action_remove">移除</string>
<string name="lock_account_label">保護你的帳戶(鎖嘟)</string>
<string name="lock_account_label_description">你需要手動審核所有關注請求</string>
<string name="compose_save_draft">儲存為草稿?</string>

View file

@ -337,7 +337,7 @@
<string name="error_failed_set_caption">设置图片标题失败</string>
<string name="hint_describe_for_visually_impaired">为视觉障碍用户提供的描述\n(限制 %d 字)</string>
<string name="action_set_caption">设置图片标题</string>
<string name="action_remove_media">移除</string>
<string name="action_remove">移除</string>
<string name="lock_account_label">保护你的帐户(锁嘟)</string>
<string name="lock_account_label_description">你需要手动审核所有关注请求</string>
<string name="compose_save_draft">保存为草稿?</string>

View file

@ -331,7 +331,7 @@
<string name="error_failed_set_caption">設定圖片標題失敗</string>
<string name="hint_describe_for_visually_impaired">為視覺障礙用戶提供的描述\n(限制 %d 字)</string>
<string name="action_set_caption">設定圖片標題</string>
<string name="action_remove_media">移除</string>
<string name="action_remove">移除</string>
<string name="lock_account_label">保護你的帳戶(鎖嘟)</string>
<string name="lock_account_label_description">你需要手動審核所有關注請求</string>
<string name="compose_save_draft">儲存為草稿?</string>

View file

@ -42,4 +42,6 @@
<dimen name="min_report_button_width">160dp</dimen>
<dimen name="card_radius">5dp</dimen>
<dimen name="poll_preview_padding">12dp</dimen>
</resources>

View file

@ -116,4 +116,27 @@
</string-array>
<string name="rick_roll_url">https://www.youtube.com/watch?v=dQw4w9WgXcQ</string>
<string-array name="poll_duration_names">
<item>@string/poll_duration_5_min</item>
<item>@string/poll_duration_30_min</item>
<item>@string/poll_duration_1_hour</item>
<item>@string/poll_duration_6_hours</item>
<item>@string/poll_duration_1_day</item>
<item>@string/poll_duration_3_days</item>
<item>@string/poll_duration_7_days</item>
</string-array>
<integer-array name="poll_duration_values"> <!-- values in seconds, corresponding to poll_duration_names -->
<item>300</item>
<item>1800</item>
<item>3600</item>
<item>21600</item>
<item>86400</item>
<item>259200</item>
<item>604800</item>
</integer-array>
</resources>

View file

@ -97,6 +97,7 @@
<string name="action_view_media">Media</string>
<string name="action_open_in_web">Open in browser</string>
<string name="action_add_media">Add media</string>
<string name="action_add_poll">Add poll</string>
<string name="action_photo_take">Take photo</string>
<string name="action_share">Share</string>
<string name="action_mute">Mute</string>
@ -349,7 +350,7 @@
<string name="error_failed_set_caption">Failed to set caption</string>
<string name="hint_describe_for_visually_impaired">Describe for visually impaired\n(%d character limit)</string>
<string name="action_set_caption">Set caption</string>
<string name="action_remove_media">Remove</string>
<string name="action_remove">Remove</string>
<string name="lock_account_label">Lock account</string>
<string name="lock_account_label_description">Requires you to manually approve followers</string>
<string name="compose_save_draft">Save draft?</string>
@ -517,4 +518,19 @@
<string name="failed_search">Failed to search</string>
<string name="pref_title_show_notifications_filter">Show Notifications filter</string>
<string name="create_poll_title">Poll</string>
<string name="poll_duration_5_min">5 minutes</string>
<string name="poll_duration_30_min">30 minutes</string>
<string name="poll_duration_1_hour">1 hour</string>
<string name="poll_duration_6_hours">6 hours</string>
<string name="poll_duration_1_day">1 day</string>
<string name="poll_duration_3_days">3 days</string>
<string name="poll_duration_7_days">7 days</string>
<string name="add_poll_choice">Add choice</string>
<string name="poll_allow_multiple_choices">Multiple choices</string>
<string name="poll_new_choice_hint">Choice %d</string>
<string name="edit_poll">Edit</string>
</resources>

View file

@ -28,7 +28,6 @@ import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils
import okhttp3.Request
import okhttp3.ResponseBody
import org.junit.Assert
import org.junit.Assert.*
import org.junit.Before
@ -258,6 +257,7 @@ class ComposeActivityTest {
emptyList()
),
maximumTootCharacters,
null,
null
)
}