added initial v2 support, VideoView fix for pre-Lollipop, fixed memory leak, added option to show user favoriting a tweet, gradle update, added strings

This commit is contained in:
nuclearfog 2021-12-21 16:49:17 +01:00
parent 086234944e
commit 1d0b1daae1
No known key found for this signature in database
GPG Key ID: AA0271FBE406DB98
16 changed files with 220 additions and 105 deletions

View File

@ -6,7 +6,7 @@ plugins {
android {
compileSdkVersion 31
buildToolsVersion '31.0.0'
buildToolsVersion '32.0.0'
defaultConfig {
applicationId 'org.nuclearfog.twidda'
@ -35,6 +35,7 @@ android {
}
packagingOptions {
exclude '**/twitter4j.properties.template'
exclude '/META-INF/CHANGES'
exclude '/META-INF/DEPENDENCIES'
exclude '/META-INF/README.md'
@ -64,6 +65,7 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'org.twitter4j:twitter4j-core:4.0.7'
implementation 'com.github.takke:twitter4j-v2:1.0.3'
//noinspection GradleDependency
implementation 'com.squareup.picasso:picasso:2.8'
implementation 'com.larswerkman:LicenseView:1.1'

View File

@ -1,18 +1,5 @@
package org.nuclearfog.twidda.activities;
import static android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START;
import static android.media.MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START;
import static android.os.AsyncTask.Status.RUNNING;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
@ -25,6 +12,7 @@ import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnInfoListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
@ -48,6 +36,7 @@ import org.nuclearfog.twidda.R;
import org.nuclearfog.twidda.adapter.ImageAdapter;
import org.nuclearfog.twidda.adapter.ImageAdapter.OnImageClickListener;
import org.nuclearfog.twidda.backend.ImageLoader;
import org.nuclearfog.twidda.backend.SeekbarUpdater;
import org.nuclearfog.twidda.backend.engine.EngineException;
import org.nuclearfog.twidda.backend.holder.ImageHolder;
import org.nuclearfog.twidda.backend.utils.AppStyles;
@ -56,10 +45,18 @@ import org.nuclearfog.twidda.backend.utils.StringTools;
import org.nuclearfog.twidda.database.GlobalSettings;
import org.nuclearfog.zoomview.ZoomView;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END;
import static android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START;
import static android.media.MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START;
import static android.os.AsyncTask.Status.RUNNING;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
/**
* Media viewer activity for images and videos
@ -115,11 +112,9 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
IDLE
}
private WeakReference<MediaViewer> updateEvent = new WeakReference<>(this);
@Nullable
private ScheduledExecutorService progressUpdate;
@Nullable
private ImageLoader imageAsync;
private SeekbarUpdater seekUpdate;
private TextView duration, position;
private ProgressBar loadingCircle;
@ -190,23 +185,15 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
case MEDIAVIEWER_VIDEO:
controlPanel.setVisibility(VISIBLE);
if (!mediaLinks[0].startsWith("http"))
if (mediaLinks[0].startsWith("/"))
share.setVisibility(GONE); // local image
final Runnable seekUpdate = new Runnable() {
public void run() {
if (updateEvent.get() != null) {
updateEvent.get().updateSeekBar();
}
}
};
progressUpdate = Executors.newScheduledThreadPool(1);
progressUpdate.scheduleWithFixedDelay(new Runnable() {
public void run() {
if (updateEvent.get() != null) {
updateEvent.get().runOnUiThread(seekUpdate);
}
}
}, PROGRESS_UPDATE, PROGRESS_UPDATE, TimeUnit.MILLISECONDS);
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && mediaLinks[0].startsWith("https://")) {
// for any reason VideoView ignores TLS 1.2 setup, so we have to use http:// instead
// todo find a solution to add TLS 1.2 support for pre lollipop devices
mediaLinks[0] = "http://" + mediaLinks[0].substring(8);
}
seekUpdate = new SeekbarUpdater(this, PROGRESS_UPDATE);
// fall through
case MEDIAVIEWER_ANGIF:
videoView.setVisibility(VISIBLE);
videoView.setZOrderMediaOverlay(true); // disable black background
@ -243,8 +230,8 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
protected void onDestroy() {
if (imageAsync != null && imageAsync.getStatus() == RUNNING)
imageAsync.cancel(true);
if (progressUpdate != null)
progressUpdate.shutdown();
if (seekUpdate != null)
seekUpdate.shutdown();
super.onDestroy();
}
@ -480,7 +467,7 @@ public class MediaViewer extends MediaActivity implements OnImageClickListener,
/**
* updates controller panel SeekBar
*/
private void updateSeekBar() {
public void updateSeekBar() {
int videoPos = video_progress.getProgress();
switch (playStat) {
case PLAY:

View File

@ -14,6 +14,7 @@ import static org.nuclearfog.twidda.activities.TweetEditor.KEY_TWEETPOPUP_REPLYI
import static org.nuclearfog.twidda.activities.TweetEditor.KEY_TWEETPOPUP_TEXT;
import static org.nuclearfog.twidda.activities.UserDetail.KEY_USERDETAIL_ID;
import static org.nuclearfog.twidda.activities.UserDetail.KEY_USERDETAIL_MODE;
import static org.nuclearfog.twidda.activities.UserDetail.USERLIST_FAVORIT;
import static org.nuclearfog.twidda.activities.UserDetail.USERLIST_RETWEETS;
import static org.nuclearfog.twidda.fragments.TweetFragment.INTENT_TWEET_REMOVED_ID;
import static org.nuclearfog.twidda.fragments.TweetFragment.INTENT_TWEET_UPDATE_DATA;
@ -203,6 +204,7 @@ public class TweetActivity extends AppCompatActivity implements OnClickListener,
replyName.setOnClickListener(this);
ansButton.setOnClickListener(this);
rtwButton.setOnClickListener(this);
favButton.setOnClickListener(this);
rtwButton.setOnLongClickListener(this);
favButton.setOnLongClickListener(this);
profile_img.setOnClickListener(this);
@ -334,6 +336,13 @@ public class TweetActivity extends AppCompatActivity implements OnClickListener,
userList.putExtra(KEY_USERDETAIL_MODE, USERLIST_RETWEETS);
startActivity(userList);
}
// show user favoriting this tweet
else if (v.getId() == R.id.tweet_favorite) {
Intent userList = new Intent(this, UserDetail.class);
userList.putExtra(KEY_USERDETAIL_ID, clickedTweet.getId());
userList.putExtra(KEY_USERDETAIL_MODE, USERLIST_FAVORIT);
startActivity(userList);
}
// open profile of the tweet author
else if (v.getId() == R.id.tweet_profile) {
Intent profile = new Intent(getApplicationContext(), UserProfile.class);

View File

@ -15,11 +15,7 @@ import org.nuclearfog.twidda.backend.utils.AppStyles;
import org.nuclearfog.twidda.database.GlobalSettings;
import org.nuclearfog.twidda.fragments.UserFragment;
import static org.nuclearfog.twidda.fragments.UserFragment.KEY_FRAG_USER_ID;
import static org.nuclearfog.twidda.fragments.UserFragment.KEY_FRAG_USER_MODE;
import static org.nuclearfog.twidda.fragments.UserFragment.USER_FRAG_FOLLOWS;
import static org.nuclearfog.twidda.fragments.UserFragment.USER_FRAG_FRIENDS;
import static org.nuclearfog.twidda.fragments.UserFragment.USER_FRAG_RETWEET;
import static org.nuclearfog.twidda.fragments.UserFragment.*;
/**
* Activity to show a list of twitter users
@ -54,6 +50,12 @@ public class UserDetail extends AppCompatActivity {
*/
public static final int USERLIST_RETWEETS = 0x19F582E;
/**
* user favoriting/liking a tweet, requires tweet ID
*/
public static final int USERLIST_FAVORIT = 0x9bcc3f99;
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(AppStyles.setFontScale(newBase));
@ -72,6 +74,7 @@ public class UserDetail extends AppCompatActivity {
int mode = data.getIntExtra(KEY_USERDETAIL_MODE, 0);
long id = data.getLongExtra(KEY_USERDETAIL_ID, -1);
GlobalSettings settings = GlobalSettings.getInstance(this);
Bundle param = new Bundle();
switch (mode) {
@ -98,17 +101,23 @@ public class UserDetail extends AppCompatActivity {
// set toolbar title
toolbar.setTitle(R.string.toolbar_userlist_retweet);
break;
case USERLIST_FAVORIT:
// set fragment parameter
param.putLong(KEY_FRAG_USER_ID, id);
param.putInt(KEY_FRAG_USER_MODE, USER_FRAG_FAVORIT);
int title = settings.likeEnabled() ? R.string.toolbar_tweet_liker : R.string.toolbar_tweet_favoriter;
// set toolbar title
toolbar.setTitle(title);
break;
}
// insert fragment into view
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, UserFragment.class, param, "");
fragmentTransaction.commit();
// set toolbar
setSupportActionBar(toolbar);
// style activity
GlobalSettings settings = GlobalSettings.getInstance(this);
AppStyles.setTheme(root, settings.getBackgroundColor());
}
}

View File

@ -0,0 +1,51 @@
package org.nuclearfog.twidda.backend;
import org.nuclearfog.twidda.activities.MediaViewer;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* This class updates {@link MediaViewer}'s Seekbar while playing a video
*
* @author nuclearfog
*/
public class SeekbarUpdater implements Runnable {
private ScheduledExecutorService updater;
private WeakReference<MediaViewer> callback;
private Runnable seekUpdate = new Runnable() {
public void run() {
MediaViewer mediaViewer = callback.get();
if (mediaViewer != null) {
mediaViewer.updateSeekBar();
}
}
};
public SeekbarUpdater(MediaViewer callback, int milliseconds) {
this.callback = new WeakReference<>(callback);
updater = Executors.newScheduledThreadPool(1);
updater.scheduleWithFixedDelay(this, milliseconds, milliseconds, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
MediaViewer mediaViewer = callback.get();
if (mediaViewer != null) {
mediaViewer.runOnUiThread(seekUpdate);
}
}
/**
* shutdown updater
*/
public void shutdown() {
updater.shutdown();
}
}

View File

@ -99,8 +99,7 @@ public class UserLoader extends AsyncTask<Long, Void, UserList> {
return mTwitter.getRetweeter(id, cursor);
case FAVORIT:
// TODO not implemented in Twitter4J
break;
return mTwitter.getFavoriter(id, cursor);
case SEARCH:
return mTwitter.searchUsers(search, cursor);

View File

@ -12,36 +12,6 @@ import twitter4j.TwitterException;
*/
public class EngineException extends Exception {
public enum ErrorType {
RATE_LIMIT_EX,
USER_NOT_FOUND,
APP_SUSPENDED,
ACCESS_TOKEN_DEAD,
TWEET_CANT_REPLY,
RESOURCE_NOT_FOUND,
CANT_SEND_DM,
NOT_AUTHORIZED,
TWEET_TOO_LONG,
DUPLICATE_TWEET,
NO_DM_TO_USER,
DM_TOO_LONG,
TOKEN_EXPIRED,
NO_MEDIA_FOUND,
NO_LINK_DEFINED,
NO_CONNECTION,
IMAGE_NOT_LOADED,
REQUEST_CANCELLED,
ACCOUNT_UPDATE_FAILED,
ERROR_API_ACCESS_DENIED,
ERROR_NOT_DEFINED
}
enum InternalErrorType {
FILENOTFOUND,
TOKENNOTSET,
BITMAP_FAILURE
}
private final ErrorType errorType;
private String msg;
private int retryAfter;
@ -129,6 +99,8 @@ public class EngineException extends Exception {
default:
if (error.getStatusCode() == 401) {
errorType = ErrorType.NOT_AUTHORIZED;
} else if (error.getStatusCode() == 403) {
errorType = ErrorType.REQUEST_FORBIDDEN;
} else if (error.getStatusCode() == 408) {
errorType = ErrorType.REQUEST_CANCELLED;
} else if (error.isCausedByNetworkIssue()) {
@ -146,7 +118,7 @@ public class EngineException extends Exception {
}
/**
* Constructor for non Twitter4J errors
* Constructor for app errors
*
* @param errorCode custom error code
*/
@ -170,6 +142,7 @@ public class EngineException extends Exception {
}
}
@Override
public String getMessage() {
if (msg == null || msg.isEmpty())
@ -195,7 +168,6 @@ public class EngineException extends Exception {
return errorType == RESOURCE_NOT_FOUND || errorType == USER_NOT_FOUND;
}
/**
* return time to wait after unlock access in seconds
*
@ -204,4 +176,41 @@ public class EngineException extends Exception {
public int getTimeToWait() {
return retryAfter;
}
/**
* enum of error types used by this class
*/
public enum ErrorType {
RATE_LIMIT_EX,
USER_NOT_FOUND,
APP_SUSPENDED,
ACCESS_TOKEN_DEAD,
TWEET_CANT_REPLY,
RESOURCE_NOT_FOUND,
CANT_SEND_DM,
NOT_AUTHORIZED,
TWEET_TOO_LONG,
DUPLICATE_TWEET,
NO_DM_TO_USER,
DM_TOO_LONG,
TOKEN_EXPIRED,
NO_MEDIA_FOUND,
NO_LINK_DEFINED,
NO_CONNECTION,
IMAGE_NOT_LOADED,
REQUEST_CANCELLED,
REQUEST_FORBIDDEN,
ACCOUNT_UPDATE_FAILED,
ERROR_API_ACCESS_DENIED,
ERROR_NOT_DEFINED
}
/**
* error types only accessible by {@link TwitterEngine}
*/
enum InternalErrorType {
FILENOTFOUND,
TOKENNOTSET,
BITMAP_FAILURE
}
}

View File

@ -42,6 +42,7 @@ import twitter4j.DirectMessage;
import twitter4j.DirectMessageList;
import twitter4j.GeoLocation;
import twitter4j.IDs;
import twitter4j.LikesExKt;
import twitter4j.Location;
import twitter4j.PagableResponseList;
import twitter4j.Paging;
@ -53,6 +54,8 @@ import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.UploadedMedia;
import twitter4j.User2;
import twitter4j.UsersResponse;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import twitter4j.conf.ConfigurationBuilder;
@ -847,7 +850,7 @@ public class TwitterEngine {
* Get User who retweeted a Tweet
*
* @param tweetID Tweet ID
* @return List of users or empty list if no match
* @return List of users
* @throws EngineException if Access is unavailable
*/
public UserList getRetweeter(long tweetID, long cursor) throws EngineException {
@ -867,6 +870,24 @@ public class TwitterEngine {
}
}
/**
* get user who liked a tweet
*
* @param tweetId Tweet ID
* @return list of users liking a tweet
* @throws EngineException if Access is unavailable
*/
public UserList getFavoriter(long tweetId, long cursor) throws EngineException {
try {
UsersResponse response = LikesExKt.getLikingUsers(twitter, tweetId, null, null, null);
List<User2> users = response.getUsers();
UserList result = new UserList(cursor, 0);
result.addAll(convertUser2List(users));
return result;
} catch (TwitterException err) {
throw new EngineException(err);
}
}
/**
* get list of Direct Messages
@ -1278,7 +1299,7 @@ public class TwitterEngine {
}
/**
* convert {@link twitter4j.User} to User List and filter excluded users
* convert {@link twitter4j.User} list to User List and filter excluded users
*
* @param users Twitter4J user List
* @return User
@ -1295,6 +1316,24 @@ public class TwitterEngine {
return result;
}
/**
* convert {@link User2} list to user list
*
* @param users list of user from version 2
* @return user list
*/
private List<User> convertUser2List(List<User2> users) throws TwitterException {
long id = twitter.getId();
ArrayList<User> result = new ArrayList<>();
result.ensureCapacity(users.size());
for (User2 user : users) {
if (user.getPublicMetrics() != null) {
result.add(new User(user, user.getPublicMetrics(), id));
}
}
return result;
}
/**
* create paging for tweets
*

View File

@ -359,7 +359,7 @@ public class Tweet implements Serializable {
private void setTweet(Status status, long twitterId) {
tweetID = status.getId();
time = status.getCreatedAt().getTime();
user = new User(status.getUser(), status.getUser().getId() == twitterId);
user = new User(status.getUser(), twitterId);
replyID = status.getInReplyToStatusId();
replyUserId = status.getInReplyToUserId();
sensitiveMedia = status.isPossiblySensitive();

View File

@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
import java.io.Serializable;
import twitter4j.URLEntity;
import twitter4j.User2;
/**
* Container class for a twitter user
@ -38,19 +39,12 @@ public class User implements Serializable {
private String profileImg = "";
private String bannerImg = "";
/**
* @param user Twitter user
* @param twitterId ID of the current user
*/
public User(twitter4j.User user, long twitterId) {
this(user, user.getId() == twitterId);
}
/**
* @param user Twitter user
* @param isCurrentUser true if user is the authenticated user
* @param user Twitter user
* @param twitterId ID of the current user
*/
public User(twitter4j.User user, boolean isCurrentUser) {
public User(twitter4j.User user, long twitterId) {
String bannerLink = user.getProfileBannerURL();
String bio = user.getDescription();
@ -85,7 +79,16 @@ public class User implements Serializable {
favorCount = user.getFavouritesCount();
isFollowReqSent = user.isFollowRequestSent();
hasDefaultImage = user.isDefaultProfileImage();
this.isCurrentUser = isCurrentUser;
isCurrentUser = twitterId == userID;
}
public User(User2 user, User2.PublicMetrics metrics, long id) {
this(user.getId(), user.getUsername(), user.getName(), user.getProfileImageUrl(),
user.getDescription(), user.getLocation(), id, user.getVerified(),
user.getProtected(), false, true, "", "", user.getCreatedAt().getTime(),
metrics.getFollowingCount(), metrics.getFollowersCount(),
metrics.getTweetCount(), metrics.getListedCount());
}
/**
@ -95,7 +98,7 @@ public class User implements Serializable {
* @param profileImg profile image link
* @param bio bio of the user
* @param location location name
* @param isCurrentUser true if this user is the authenticated user
* @param currentId current ID of the user
* @param isVerified true if user is verified
* @param isLocked true if users profile is locked
* @param isFollowReqSent true if authenticated user has sent a follow request
@ -108,7 +111,7 @@ public class User implements Serializable {
* @param tweetCount number of tweets of the user
* @param favorCount number of tweets favored by the user
*/
public User(long userID, String username, String screenName, String profileImg, String bio, String location, boolean isCurrentUser,
public User(long userID, String username, String screenName, String profileImg, String bio, String location, long currentId,
boolean isVerified, boolean isLocked, boolean isFollowReqSent, boolean hasDefaultImage, String link,
String bannerImg, long created, int following, int follower, int tweetCount, int favorCount) {
@ -127,7 +130,6 @@ public class User implements Serializable {
if (bannerImg != null)
this.bannerImg = bannerImg;
this.userID = userID;
this.isCurrentUser = isCurrentUser;
this.isVerified = isVerified;
this.isLocked = isLocked;
this.created = created;
@ -137,6 +139,7 @@ public class User implements Serializable {
this.favorCount = favorCount;
this.isFollowReqSent = isFollowReqSent;
this.hasDefaultImage = hasDefaultImage;
isCurrentUser = currentId == userID;
}
/**

View File

@ -103,6 +103,9 @@ public final class ErrorHandler {
case REQUEST_CANCELLED:
return context.getString(R.string.error_result_cancelled);
case REQUEST_FORBIDDEN:
return context.getString(R.string.error_forbidden_api_access);
case APP_SUSPENDED:
case ERROR_API_ACCESS_DENIED:
GlobalSettings settings = GlobalSettings.getInstance(context);

View File

@ -15,7 +15,7 @@ import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* Enable Experimental TLS 1.2 support for devices lower than android 21
* Enable Experimental TLS 1.2 support for pre-Lollipop devices
*
* @author fkrauthan
* @see <a href="https://gist.githubusercontent.com/fkrauthan/ac8624466a4dee4fd02f/raw/309efc30e31c96a932ab9d19bf4d73b286b00573/TLSSocketFactory.java"/>
@ -62,7 +62,7 @@ public class TLSSocketFactory extends SSLSocketFactory {
*
*/
TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
SSLContext context = SSLContext.getInstance(TLS_1_2);
context.init(null, null, null);
internalSSLSocketFactory = context.getSocketFactory();
}

View File

@ -754,12 +754,11 @@ public class AppDatabase {
int tCount = cursor.getInt(cursor.getColumnIndexOrThrow(UserTable.TWEETS));
int fCount = cursor.getInt(cursor.getColumnIndexOrThrow(UserTable.FAVORS));
int userRegister = cursor.getInt(cursor.getColumnIndexOrThrow(UserRegisterTable.REGISTER));
boolean isCurrentUser = homeId == userId;
boolean isVerified = (userRegister & VER_MASK) != 0;
boolean isLocked = (userRegister & LCK_MASK) != 0;
boolean isReq = (userRegister & FRQ_MASK) != 0;
boolean defaultImg = (userRegister & DEF_IMG) != 0;
return new User(userId, username, screenName, profileImg, bio, location, isCurrentUser, isVerified,
return new User(userId, username, screenName, profileImg, bio, location, homeId, isVerified,
isLocked, isReq, defaultImg, link, banner, createdAt, following, follower, tCount, fCount);
}

View File

@ -80,7 +80,6 @@ public class UserFragment extends ListFragment implements UserClickListener,
/**
* configuration to get a list of users favoring a tweet
* todo implement this function if there is an API for it
*/
public static final int USER_FRAG_FAVORIT = 0xA7FB2BB4;

View File

@ -223,4 +223,7 @@
<string name="info_tweet_liked">Tweet zu den Likes hinzugefügt</string>
<string name="info_tweet_unliked">Tweet aus den Likes entfernt</string>
<string name="app_info_icons">svg Icons von:</string>
<string name="toolbar_tweet_favoriter">Tweet favorisiert von</string>
<string name="toolbar_tweet_liker">Tweet gelikt von</string>
<string name="error_forbidden_api_access">Diese API unterstützt nicht diese Aktion!</string>
</resources>

View File

@ -124,6 +124,8 @@
<string name="confirm_remove_account">remove account from list?</string>
<string name="account_user_id_prefix" translatable="false">User ID:</string>
<string name="account_user_unnamed">\'unnamed\'</string>
<string name="toolbar_tweet_favoriter">User favoriting this tweet</string>
<string name="toolbar_tweet_liker">User liking this tweet</string>
<!-- toast messages to inform user -->
<string name="info_user_removed">User removed from list</string>
@ -163,6 +165,7 @@
<string name="info_permission_read">Read permission needed to access images and videos.</string>
<string name="info_permission_location">Location permission only needed to add location information to tweets.</string>
<string name="info_permission_write">Write permission used to store images.</string>
<string name="info_restart_app_on_change">restarting required to apply changes</string>
<string name="info_error">Error</string>
<!-- toast messages for error information -->
@ -205,6 +208,7 @@
<string name="error_result_cancelled">Error, result cancelled!</string>
<string name="error_twitter_search">Error, search query is too long or contains illegal characters!</string>
<string name="error_not_defined">Not specified error!</string>
<string name="error_forbidden_api_access">API does not support this operation!</string>
<!-- menu icon strings -->
<string name="menu_tweet">write Tweet</string>
@ -246,7 +250,6 @@
<string name="dialog_button_ok">OK</string>
<string name="dialog_link_image_preview">Link preview image</string>
<string name="dialog_link_close">close link preview</string>
<string name="info_restart_app_on_change">restarting required to apply changes</string>
<string name="app_info_icons">svg icons from:</string>
<string name="app_info_icons_links" translatable="false">www.svgrepo.com www.entypo.com</string>