Merge remote-tracking branch 'origin/maintenance' into maintenance

This commit is contained in:
Tlaster 2020-04-13 00:42:35 +08:00
commit fc2bbe857b
66 changed files with 1829 additions and 133 deletions

View File

@ -6,6 +6,8 @@
Material Design ready and feature rich Twitter app for Android 4.0+ Material Design ready and feature rich Twitter app for Android 4.0+
Twidere-Android is maintained by community and supporter including [Dimension](https://dimension.im/).
--- ---
## Features ## ## Features ##

View File

@ -17,8 +17,8 @@ allprojects {
ext { ext {
projectGroupId = 'org.mariotaku.twidere' projectGroupId = 'org.mariotaku.twidere'
projectVersionCode = 502 projectVersionCode = 505
projectVersionName = '4.0.3' projectVersionName = '4.0.6'
globalCompileSdkVersion = 29 globalCompileSdkVersion = 29
globalBuildToolsVersion = '29.0.2' globalBuildToolsVersion = '29.0.2'

View File

@ -41,5 +41,5 @@ public interface Twitter extends SearchResources, TimelineResources, TweetResour
ListResources, DirectMessagesResources, DirectMessagesEventResources, ListResources, DirectMessagesResources, DirectMessagesEventResources,
FriendsFollowersResources, FavoritesResources, SpamReportingResources, FriendsFollowersResources, FavoritesResources, SpamReportingResources,
SavedSearchesResources, TrendsResources, PlacesGeoResources, SavedSearchesResources, TrendsResources, PlacesGeoResources,
HelpResources, MutesResources { HelpResources, MutesResources, TwitterPrivate {
} }

View File

@ -0,0 +1,35 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter;
import org.mariotaku.microblog.library.twitter.api.PrivateAccountResources;
import org.mariotaku.microblog.library.twitter.api.PrivateActivityResources;
import org.mariotaku.microblog.library.twitter.api.PrivateDirectMessagesResources;
import org.mariotaku.microblog.library.twitter.api.PrivateFriendsFollowersResources;
import org.mariotaku.microblog.library.twitter.api.PrivateSearchResources;
import org.mariotaku.microblog.library.twitter.api.PrivateTimelineResources;
import org.mariotaku.microblog.library.twitter.api.PrivateTweetResources;
/**
* Created by mariotaku on 16/3/4.
*/
public interface TwitterPrivate extends PrivateActivityResources, PrivateTweetResources,
PrivateTimelineResources, PrivateFriendsFollowersResources, PrivateDirectMessagesResources,
PrivateSearchResources, PrivateAccountResources {
}

View File

@ -0,0 +1,37 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.PinTweetResult;
import org.mariotaku.restfu.annotation.method.POST;
import org.mariotaku.restfu.annotation.param.Param;
/**
* Created by mariotaku on 16/8/20.
*/
public interface PrivateAccountResources extends PrivateResources {
@POST("/account/pin_tweet.json")
PinTweetResult pinTweet(@Param("id") String id) throws MicroBlogException;
@POST("/account/unpin_tweet.json")
PinTweetResult unpinTweet(@Param("id") String id) throws MicroBlogException;
}

View File

@ -0,0 +1,52 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Activity;
import org.mariotaku.microblog.library.twitter.model.CursorTimestampResponse;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.microblog.library.twitter.template.StatusAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.method.POST;
import org.mariotaku.restfu.annotation.param.Param;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Queries;
import org.mariotaku.restfu.annotation.param.Query;
import org.mariotaku.restfu.http.BodyType;
@SuppressWarnings("RedundantThrows")
@Params(template = StatusAnnotationTemplate.class)
public interface PrivateActivityResources extends PrivateResources {
@GET("/activity/about_me.json")
ResponseList<Activity> getActivitiesAboutMe(@Query Paging paging) throws MicroBlogException;
@Queries({})
@GET("/activity/about_me/unread.json")
CursorTimestampResponse getActivitiesAboutMeUnread(@Query("cursor") boolean cursor) throws MicroBlogException;
@Queries({})
@POST("/activity/about_me/unread.json")
@BodyType(BodyType.FORM)
CursorTimestampResponse setActivitiesAboutMeUnread(@Param("cursor") long cursor) throws MicroBlogException;
}

View File

@ -0,0 +1,102 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import androidx.annotation.Nullable;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.ConversationTimeline;
import org.mariotaku.microblog.library.twitter.model.DMResponse;
import org.mariotaku.microblog.library.twitter.model.NewDm;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseCode;
import org.mariotaku.microblog.library.twitter.model.UserEvents;
import org.mariotaku.microblog.library.twitter.model.UserInbox;
import org.mariotaku.microblog.library.twitter.template.DMAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.method.POST;
import org.mariotaku.restfu.annotation.param.Param;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Path;
import org.mariotaku.restfu.annotation.param.Query;
import org.mariotaku.restfu.http.BodyType;
@Params(template = DMAnnotationTemplate.class)
public interface PrivateDirectMessagesResources extends PrivateResources {
@POST("/dm/conversation/{conversation_id}/delete.json")
@BodyType(BodyType.FORM)
ResponseCode deleteDmConversation(@Path("conversation_id") String conversationId)
throws MicroBlogException;
@POST("/dm/conversation/{conversation_id}/mark_read.json")
@BodyType(BodyType.FORM)
ResponseCode markDmRead(@Path("conversation_id") String conversationId,
@Param("last_read_event_id") String lastReadEventId) throws MicroBlogException;
@POST("/dm/update_last_seen_event_id.json")
@BodyType(BodyType.FORM)
ResponseCode updateLastSeenEventId(@Param("last_seen_event_id") String lastSeenEventId) throws MicroBlogException;
@POST("/dm/conversation/{conversation_id}/update_name.json")
@BodyType(BodyType.FORM)
ResponseCode updateDmConversationName(@Path("conversation_id") String conversationId,
@Param("name") String name) throws MicroBlogException;
/**
* Update DM conversation avatar
*
* @param conversationId DM conversation ID
* @param avatarId Avatar media ID, null for removing avatar
* @return HTTP response code
*/
@POST("/dm/conversation/{conversation_id}/update_avatar.json")
@BodyType(BodyType.FORM)
ResponseCode updateDmConversationAvatar(@Path("conversation_id") String conversationId,
@Param(value = "avatar_id", ignoreOnNull = true) @Nullable String avatarId) throws MicroBlogException;
@POST("/dm/conversation/{conversation_id}/disable_notifications.json")
ResponseCode disableDmConversations(@Path("conversation_id") String conversationId)
throws MicroBlogException;
@POST("/dm/conversation/{conversation_id}/enable_notifications.json")
ResponseCode enableDmConversations(@Path("conversation_id") String conversationId)
throws MicroBlogException;
@POST("/dm/new.json")
DMResponse sendDm(@Param NewDm newDm) throws MicroBlogException;
@POST("/dm/conversation/{conversation_id}/add_participants.json")
DMResponse addParticipants(@Path("conversation_id") String conversationId,
@Param(value = "participant_ids", arrayDelimiter = ',') String[] participantIds)
throws MicroBlogException;
@POST("/dm/destroy.json")
ResponseCode destroyDm(@Param("dm_id") String id) throws MicroBlogException;
@GET("/dm/user_inbox.json")
UserInbox getUserInbox(@Query Paging paging) throws MicroBlogException;
@GET("/dm/user_updates.json")
UserEvents getUserUpdates(@Query("cursor") String cursor) throws MicroBlogException;
@GET("/dm/conversation/{conversation_id}.json")
ConversationTimeline getDmConversation(@Path("conversation_id") String conversationId,
@Query Paging paging) throws MicroBlogException;
}

View File

@ -0,0 +1,43 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.restfu.annotation.method.POST;
import org.mariotaku.restfu.annotation.param.KeyValue;
import org.mariotaku.restfu.annotation.param.Param;
import org.mariotaku.restfu.annotation.param.Queries;
@Queries({@KeyValue(key = "include_entities", valueKey = "include_entities")})
public interface PrivateFriendsFollowersResources extends PrivateResources {
@POST("/friendships/accept.json")
User acceptFriendship(@Param("user_id") String userId) throws MicroBlogException;
@POST("/friendships/accept.json")
User acceptFriendshipByScreenName(@Param("screen_name") String screenName) throws MicroBlogException;
@POST("/friendships/deny.json")
User denyFriendship(@Param("user_id") String userId) throws MicroBlogException;
@POST("/friendships/deny.json")
User denyFriendshipByScreenName(@Param("screen_name") String screenName) throws MicroBlogException;
}

View File

@ -0,0 +1,42 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.IDs;
import org.mariotaku.microblog.library.twitter.model.MutedKeyword;
import org.mariotaku.microblog.library.twitter.model.PageableResponseList;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.template.UserAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Query;
/**
* Created by mariotaku on 2017/3/26.
*/
@Params(template = UserAnnotationTemplate.class)
public interface PrivateMutesResources {
@GET("/mutes/keywords/ids.json")
IDs getMutesKeywordsIDs(Paging paging) throws MicroBlogException;
@GET("/mutes/keywords/list.json")
PageableResponseList<MutedKeyword> getMutesKeywordsList(@Query Paging paging) throws MicroBlogException;
}

View File

@ -0,0 +1,23 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
public interface PrivateResources {
}

View File

@ -0,0 +1,38 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.UniversalSearchQuery;
import org.mariotaku.microblog.library.twitter.model.UniversalSearchResult;
import org.mariotaku.microblog.library.twitter.template.StatusAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Query;
/**
* Created by mariotaku on 15/10/21.
*/
public interface PrivateSearchResources extends PrivateResources {
@GET("/search/universal.json")
@Params(template = StatusAnnotationTemplate.class)
UniversalSearchResult universalSearch(@Query UniversalSearchQuery query) throws MicroBlogException;
}

View File

@ -0,0 +1,39 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.microblog.library.twitter.template.StatusAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Query;
@SuppressWarnings("RedundantThrows")
@Params(template = StatusAnnotationTemplate.class)
public interface PrivateTimelineResources extends PrivateResources {
@GET("/statuses/media_timeline.json")
ResponseList<Status> getMediaTimeline(@Query("user_id") String userId, @Query Paging paging) throws MicroBlogException;
@GET("/statuses/media_timeline.json")
ResponseList<Status> getMediaTimelineByScreenName(@Query("screen_name") String screenName, @Query Paging paging) throws MicroBlogException;
}

View File

@ -0,0 +1,45 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.twitter.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.microblog.library.twitter.model.ResponseList;
import org.mariotaku.microblog.library.twitter.model.Status;
import org.mariotaku.microblog.library.twitter.model.StatusActivitySummary;
import org.mariotaku.microblog.library.twitter.model.TranslationResult;
import org.mariotaku.microblog.library.twitter.template.StatusAnnotationTemplate;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Params;
import org.mariotaku.restfu.annotation.param.Path;
import org.mariotaku.restfu.annotation.param.Query;
@SuppressWarnings("RedundantThrows")
@Params(template = StatusAnnotationTemplate.class)
public interface PrivateTweetResources extends PrivateResources {
@GET("/statuses/{id}/activity/summary.json")
StatusActivitySummary getStatusActivitySummary(@Path("id") String statusId) throws MicroBlogException;
@GET("/conversation/show.json")
ResponseList<Status> showConversation(@Query("id") String statusId, @Query Paging paging) throws MicroBlogException;
@GET("/translations/show.json")
TranslationResult showTranslation(@Query("id") String statusId, @Query("dest") String dest) throws MicroBlogException;
}

View File

@ -24,7 +24,8 @@ import androidx.annotation.NonNull;
* Created by mariotaku on 15/4/20. * Created by mariotaku on 15/4/20.
*/ */
public enum ConsumerKeyType { public enum ConsumerKeyType {
UNKNOWN; TWITTER_FOR_ANDROID, TWITTER_FOR_IPHONE, TWITTER_FOR_IPAD, TWITTER_FOR_MAC,
TWITTER_FOR_WINDOWS_PHONE, TWITTER_FOR_GOOGLE_TV, TWEETDECK, UNKNOWN;
@NonNull @NonNull
public static ConsumerKeyType parse(String type) { public static ConsumerKeyType parse(String type) {

View File

@ -21,8 +21,10 @@ package org.mariotaku.twidere.model.account;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject; import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease; import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
/** /**
* Created by mariotaku on 16/2/26. * Created by mariotaku on 16/2/26.
@ -45,6 +47,17 @@ public class TwitterAccountExtras implements Parcelable, AccountExtras {
} }
}; };
@JsonField(name = "official_credentials")
@ParcelableThisPlease
boolean officialCredentials;
public boolean isOfficialCredentials() {
return officialCredentials;
}
public void setIsOfficialCredentials(boolean officialCredentials) {
this.officialCredentials = officialCredentials;
}
@Override @Override
public int describeContents() { public int describeContents() {
@ -59,6 +72,7 @@ public class TwitterAccountExtras implements Parcelable, AccountExtras {
@Override @Override
public String toString() { public String toString() {
return "TwitterAccountExtras{" + return "TwitterAccountExtras{" +
"officialCredentials=" + officialCredentials +
'}'; '}';
} }
} }

View File

@ -216,6 +216,7 @@ dependencies {
implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation "androidx.drawerlayout:drawerlayout:1.1.0-alpha01"
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.exifinterface:exifinterface:1.1.0' implementation 'androidx.exifinterface:exifinterface:1.1.0'
implementation "com.twitter:twitter-text:${libVersions['TwitterText']}" implementation "com.twitter:twitter-text:${libVersions['TwitterText']}"

View File

@ -58,6 +58,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.NFC"/> <uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Used for account management --> <!-- Used for account management -->
<uses-permission <uses-permission

View File

@ -129,6 +129,26 @@ public class AccountUtils {
return null; return null;
} }
public static boolean isOfficial(@Nullable final Context context, @NonNull final UserKey accountKey) {
if (context == null) {
return false;
}
AccountManager am = AccountManager.get(context);
Account account = AccountUtils.findByAccountKey(am, accountKey);
if (account == null) return false;
return AccountExtensionsKt.isOfficial(account, am, context);
}
public static boolean hasOfficialKeyAccount(Context context) {
final AccountManager am = AccountManager.get(context);
for (Account account : getAccounts(am)) {
if (AccountExtensionsKt.isOfficial(account, am, context)) {
return true;
}
}
return false;
}
public static int getAccountTypeIcon(@Nullable String accountType) { public static int getAccountTypeIcon(@Nullable String accountType) {
if (accountType == null) return R.drawable.ic_account_logo_twitter; if (accountType == null) return R.drawable.ic_account_logo_twitter;
switch (accountType) { switch (accountType) {

View File

@ -2,6 +2,7 @@ package org.mariotaku.twidere.util;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import org.mariotaku.microblog.library.twitter.model.DMResponse; import org.mariotaku.microblog.library.twitter.model.DMResponse;
@ -9,9 +10,14 @@ import org.mariotaku.microblog.library.twitter.model.DirectMessage;
import org.mariotaku.microblog.library.twitter.model.MediaEntity; import org.mariotaku.microblog.library.twitter.model.MediaEntity;
import org.mariotaku.microblog.library.twitter.model.UrlEntity; import org.mariotaku.microblog.library.twitter.model.UrlEntity;
import org.mariotaku.microblog.library.twitter.model.User; import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.extension.model.api.StatusExtensionsKt; import org.mariotaku.twidere.extension.model.api.StatusExtensionsKt;
import org.mariotaku.twidere.model.ConsumerKeyType;
import org.mariotaku.twidere.model.SpanItem; import org.mariotaku.twidere.model.SpanItem;
import java.nio.charset.Charset;
import java.util.zip.CRC32;
import kotlin.Pair; import kotlin.Pair;
/** /**
@ -96,4 +102,55 @@ public class InternalTwitterContentUtils {
} }
public static boolean isOfficialKey(final Context context, final String consumerKey,
final String consumerSecret) {
if (context == null || consumerKey == null || consumerSecret == null) return false;
final String[] keySecrets = context.getResources().getStringArray(R.array.values_official_consumer_secret_crc32);
final CRC32 crc32 = new CRC32();
final byte[] consumerSecretBytes = consumerSecret.getBytes(Charset.forName("UTF-8"));
crc32.update(consumerSecretBytes, 0, consumerSecretBytes.length);
final long value = crc32.getValue();
crc32.reset();
for (final String keySecret : keySecrets) {
if (Long.parseLong(keySecret, 16) == value) return true;
}
return false;
}
public static String getOfficialKeyName(final Context context, final String consumerKey,
final String consumerSecret) {
if (context == null || consumerKey == null || consumerSecret == null) return null;
final String[] keySecrets = context.getResources().getStringArray(R.array.values_official_consumer_secret_crc32);
final String[] keyNames = context.getResources().getStringArray(R.array.names_official_consumer_secret);
final CRC32 crc32 = new CRC32();
final byte[] consumerSecretBytes = consumerSecret.getBytes(Charset.forName("UTF-8"));
crc32.update(consumerSecretBytes, 0, consumerSecretBytes.length);
final long value = crc32.getValue();
crc32.reset();
for (int i = 0, j = keySecrets.length; i < j; i++) {
if (Long.parseLong(keySecrets[i], 16) == value) return keyNames[i];
}
return null;
}
@NonNull
public static ConsumerKeyType getOfficialKeyType(final Context context, final String consumerKey,
final String consumerSecret) {
if (context == null || consumerKey == null || consumerSecret == null) {
return ConsumerKeyType.UNKNOWN;
}
final String[] keySecrets = context.getResources().getStringArray(R.array.values_official_consumer_secret_crc32);
final String[] keyNames = context.getResources().getStringArray(R.array.types_official_consumer_secret);
final CRC32 crc32 = new CRC32();
final byte[] consumerSecretBytes = consumerSecret.getBytes(Charset.forName("UTF-8"));
crc32.update(consumerSecretBytes, 0, consumerSecretBytes.length);
final long value = crc32.getValue();
crc32.reset();
for (int i = 0, j = keySecrets.length; i < j; i++) {
if (Long.parseLong(keySecrets[i], 16) == value) {
return ConsumerKeyType.parse(keyNames[i]);
}
}
return ConsumerKeyType.UNKNOWN;
}
} }

View File

@ -25,6 +25,9 @@ import org.mariotaku.twidere.model.ConsumerKeyType;
import org.mariotaku.twidere.model.UserKey; import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.model.account.cred.Credentials; import org.mariotaku.twidere.model.account.cred.Credentials;
import org.mariotaku.twidere.model.util.AccountUtils; import org.mariotaku.twidere.model.util.AccountUtils;
import org.mariotaku.twidere.util.api.TwitterAndroidExtraHeaders;
import org.mariotaku.twidere.util.api.TwitterMacExtraHeaders;
import org.mariotaku.twidere.util.api.UserAgentExtraHeaders;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -166,6 +169,21 @@ public class MicroBlogAPIFactory implements TwidereConstants {
@WorkerThread @WorkerThread
@Nullable @Nullable
public static ExtraHeaders getExtraHeaders(Context context, ConsumerKeyType type) { public static ExtraHeaders getExtraHeaders(Context context, ConsumerKeyType type) {
switch (type) {
case TWITTER_FOR_ANDROID: {
return TwitterAndroidExtraHeaders.INSTANCE;
}
case TWITTER_FOR_IPHONE:
case TWITTER_FOR_IPAD: {
return new UserAgentExtraHeaders("Twitter/6.75.2 CFNetwork/811.4.18 Darwin/16.5.0");
}
case TWITTER_FOR_MAC: {
return TwitterMacExtraHeaders.INSTANCE;
}
case TWEETDECK: {
return new UserAgentExtraHeaders(UserAgentUtils.getDefaultUserAgentStringSafe(context));
}
}
return null; return null;
} }

View File

@ -1127,6 +1127,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
} else { } else {
editText.setSelection(selection.coerceIn(0..editText.length())) editText.setSelection(selection.coerceIn(0..editText.length()))
} }
editText.requestFocus()
return true return true
} }

View File

@ -26,6 +26,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Parcelable import android.os.Parcelable
import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -73,6 +74,7 @@ import org.mariotaku.twidere.util.support.WindowSupport
import org.mariotaku.twidere.view.viewer.MediaSwipeCloseContainer import org.mariotaku.twidere.view.viewer.MediaSwipeCloseContainer
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.thread
import android.Manifest.permission as AndroidPermissions import android.Manifest.permission as AndroidPermissions
class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeCloseContainer.Listener, class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeCloseContainer.Listener,
@ -504,13 +506,25 @@ class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeClos
val type = (fileInfo as? CacheProvider.CacheFileTypeSupport)?.cacheFileType val type = (fileInfo as? CacheProvider.CacheFileTypeSupport)?.cacheFileType
val pubDir = when (type) { val pubDir = when (type) {
CacheFileType.VIDEO -> { CacheFileType.VIDEO -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
} else {
getExternalFilesDir(Environment.DIRECTORY_MOVIES)
}
} }
CacheFileType.IMAGE -> { CacheFileType.IMAGE -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
} else {
getExternalFilesDir(Environment.DIRECTORY_PICTURES)
}
} }
else -> { else -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
} else {
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
}
} }
} }
val saveDir = File(pubDir, "Twidere") val saveDir = File(pubDir, "Twidere")
@ -521,17 +535,19 @@ class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeClos
private fun openSaveToDocumentChooser() { private fun openSaveToDocumentChooser() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return
val fileInfo = getCurrentCacheFileInfo(viewPager.currentItem) ?: return val fileInfo = getCurrentCacheFileInfo(viewPager.currentItem) ?: return
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) thread {
intent.type = fileInfo.mimeType ?: "*/*" val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = fileInfo.mimeType ?: "*/*"
val extension = fileInfo.fileExtension intent.addCategory(Intent.CATEGORY_OPENABLE)
val saveFileName = if (extension != null) { val extension = fileInfo.fileExtension
"${fileInfo.fileName?.removeSuffix("_$extension")}.$extension" val saveFileName = if (extension != null) {
} else { "${fileInfo.fileName?.removeSuffix("_$extension")}.$extension"
fileInfo.fileName } else {
fileInfo.fileName
}
intent.putExtra(Intent.EXTRA_TITLE, saveFileName)
startActivityForResult(intent, REQUEST_SELECT_SAVE_MEDIA)
} }
intent.putExtra(Intent.EXTRA_TITLE, saveFileName)
startActivityForResult(intent, REQUEST_SELECT_SAVE_MEDIA)
} }
private fun saveMediaToContentUri(data: Uri) { private fun saveMediaToContentUri(data: Uri) {

View File

@ -70,6 +70,7 @@ import org.mariotaku.microblog.library.mastodon.annotation.AuthScope
import org.mariotaku.microblog.library.twitter.TwitterOAuth import org.mariotaku.microblog.library.twitter.TwitterOAuth
import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization import org.mariotaku.microblog.library.twitter.auth.BasicAuthorization
import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization import org.mariotaku.microblog.library.twitter.auth.EmptyAuthorization
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.User import org.mariotaku.microblog.library.twitter.model.User
import org.mariotaku.restfu.http.Endpoint import org.mariotaku.restfu.http.Endpoint
import org.mariotaku.restfu.oauth.OAuthToken import org.mariotaku.restfu.oauth.OAuthToken
@ -410,7 +411,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
result.addAccount(am, preferences[randomizeAccountNameKey]) result.addAccount(am, preferences[randomizeAccountNameKey])
Analyzer.log(SignIn(true, accountType = result.type, Analyzer.log(SignIn(true, accountType = result.type,
credentialsType = apiConfig.credentialsType, credentialsType = apiConfig.credentialsType,
officialKey = false)) officialKey = result.extras?.official == true))
finishSignIn() finishSignIn()
} }
} }
@ -1220,7 +1221,17 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
} }
private fun getTwitterAccountExtras(twitter: MicroBlog): TwitterAccountExtras { private fun getTwitterAccountExtras(twitter: MicroBlog): TwitterAccountExtras {
return TwitterAccountExtras() val extras = TwitterAccountExtras()
try {
// Get Twitter official only resource
val paging = Paging()
paging.count(1)
twitter.getActivitiesAboutMe(paging)
extras.setIsOfficialCredentials(true)
} catch (e: MicroBlogException) {
// Ignore
}
return extras
} }
private fun getMastodonAccountExtras(mastodon: Mastodon): MastodonAccountExtras { private fun getMastodonAccountExtras(mastodon: Mastodon): MastodonAccountExtras {

View File

@ -453,7 +453,7 @@ abstract class ParcelableStatusesAdapter(
val timestamp = cursor.safeGetLong(indices[Statuses.TIMESTAMP]) val timestamp = cursor.safeGetLong(indices[Statuses.TIMESTAMP])
val sortId = cursor.safeGetLong(indices[Statuses.SORT_ID]) val sortId = cursor.safeGetLong(indices[Statuses.SORT_ID])
val positionKey = cursor.safeGetLong(indices[Statuses.POSITION_KEY]) val positionKey = cursor.safeGetLong(indices[Statuses.POSITION_KEY])
val gap = cursor.getInt(indices[Statuses.IS_GAP]) == 1 val gap = cursor.safeGetInt(indices[Statuses.IS_GAP]) == 1
val newInfo = StatusInfo(_id, accountKey, id, timestamp, sortId, positionKey, gap) val newInfo = StatusInfo(_id, accountKey, id, timestamp, sortId, positionKey, gap)
infoCache?.set(dataPosition, newInfo) infoCache?.set(dataPosition, newInfo)
return@run newInfo return@run newInfo

View File

@ -16,6 +16,30 @@ import org.mariotaku.twidere.util.text.FanfouValidator
import org.mariotaku.twidere.util.text.MastodonValidator import org.mariotaku.twidere.util.text.MastodonValidator
import org.mariotaku.twidere.util.text.TwitterValidator import org.mariotaku.twidere.util.text.TwitterValidator
fun AccountDetails.isOfficial(context: Context?): Boolean {
if (context == null) {
return false
}
val extra = this.extras
if (extra is TwitterAccountExtras) {
return extra.isOfficialCredentials
}
val credentials = this.credentials
if (credentials is OAuthCredentials) {
return InternalTwitterContentUtils.isOfficialKey(context,
credentials.consumer_key, credentials.consumer_secret)
}
return false
}
val AccountExtras.official: Boolean
get() {
if (this is TwitterAccountExtras) {
return isOfficialCredentials
}
return false
}
fun <T> AccountDetails.newMicroBlogInstance(context: Context, cls: Class<T>): T { fun <T> AccountDetails.newMicroBlogInstance(context: Context, cls: Class<T>): T {
return credentials.newMicroBlogInstance(context, type, cls) return credentials.newMicroBlogInstance(context, type, cls)
} }

View File

@ -100,6 +100,19 @@ fun Account.setPosition(am: AccountManager, position: Int) {
am.setUserData(this, ACCOUNT_USER_DATA_POSITION, position.toString()) am.setUserData(this, ACCOUNT_USER_DATA_POSITION, position.toString())
} }
fun Account.isOfficial(am: AccountManager, context: Context): Boolean {
val extras = getAccountExtras(am)
if (extras is TwitterAccountExtras) {
return extras.isOfficialCredentials
}
val credentials = getCredentials(am)
if (credentials is OAuthCredentials) {
return InternalTwitterContentUtils.isOfficialKey(context, credentials.consumer_key,
credentials.consumer_secret)
}
return false
}
fun AccountManager.hasInvalidAccount(): Boolean { fun AccountManager.hasInvalidAccount(): Boolean {
val accounts = AccountUtils.getAccounts(this) val accounts = AccountUtils.getAccounts(this)
if (accounts.isEmpty()) return false if (accounts.isEmpty()) return false

View File

@ -23,7 +23,6 @@ import org.mariotaku.restfu.oauth.OAuthToken
import org.mariotaku.restfu.oauth2.OAuth2Authorization import org.mariotaku.restfu.oauth2.OAuth2Authorization
import org.mariotaku.twidere.TwidereConstants.DEFAULT_TWITTER_API_URL_FORMAT import org.mariotaku.twidere.TwidereConstants.DEFAULT_TWITTER_API_URL_FORMAT
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.model.ConsumerKeyType
import org.mariotaku.twidere.model.account.cred.* import org.mariotaku.twidere.model.account.cred.*
import org.mariotaku.twidere.util.HttpClientFactory import org.mariotaku.twidere.util.HttpClientFactory
import org.mariotaku.twidere.util.InternalTwitterContentUtils import org.mariotaku.twidere.util.InternalTwitterContentUtils
@ -149,7 +148,9 @@ fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authori
val factory = RestAPIFactory<MicroBlogException>() val factory = RestAPIFactory<MicroBlogException>()
val extraHeaders = run { val extraHeaders = run {
if (auth !is OAuthAuthorization) return@run null if (auth !is OAuthAuthorization) return@run null
return@run MicroBlogAPIFactory.getExtraHeaders(context, ConsumerKeyType.UNKNOWN) val officialKeyType = InternalTwitterContentUtils.getOfficialKeyType(context,
auth.consumerKey, auth.consumerSecret)
return@run MicroBlogAPIFactory.getExtraHeaders(context, officialKeyType)
} ?: UserAgentExtraHeaders(MicroBlogAPIFactory.getTwidereUserAgent(context)) } ?: UserAgentExtraHeaders(MicroBlogAPIFactory.getTwidereUserAgent(context))
val holder = DependencyHolder.get(context) val holder = DependencyHolder.get(context)
var extraRequestParams: Map<String, String>? = null var extraRequestParams: Map<String, String>? = null

View File

@ -306,10 +306,13 @@ abstract class AbsActivitiesFragment protected constructor() :
override fun onGapClick(holder: GapViewHolder, position: Int) { override fun onGapClick(holder: GapViewHolder, position: Int) {
val activity = adapter.getActivity(position) val activity = adapter.getActivity(position)
DebugLog.v(msg = "Load activity gap $activity") DebugLog.v(msg = "Load activity gap $activity")
if (activity.action !in Activity.Action.MENTION_ACTIONS) { if (!AccountUtils.isOfficial(context, activity.account_key)) {
adapter.removeGapLoadingId(ObjectId(activity.account_key, activity.id)) // Skip if item is not a status
adapter.notifyItemChanged(position) if (activity.action !in Activity.Action.MENTION_ACTIONS) {
return adapter.removeGapLoadingId(ObjectId(activity.account_key, activity.id))
adapter.notifyItemChanged(position)
return
}
} }
val accountKeys = arrayOf(activity.account_key) val accountKeys = arrayOf(activity.account_key)
val pagination = arrayOf(SinceMaxPagination.maxId(activity.min_position, val pagination = arrayOf(SinceMaxPagination.maxId(activity.min_position,

View File

@ -60,6 +60,7 @@ import org.mariotaku.twidere.adapter.ArrayAdapter
import org.mariotaku.twidere.annotation.CustomTabType import org.mariotaku.twidere.annotation.CustomTabType
import org.mariotaku.twidere.annotation.TabAccountFlags import org.mariotaku.twidere.annotation.TabAccountFlags
import org.mariotaku.twidere.extension.applyTheme import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.Tab import org.mariotaku.twidere.model.Tab
import org.mariotaku.twidere.model.tab.DrawableHolder import org.mariotaku.twidere.model.tab.DrawableHolder
@ -289,9 +290,9 @@ class CustomTabsFragment : BaseFragment(), LoaderCallbacks<Cursor?>, MultiChoice
if (!accountRequired) { if (!accountRequired) {
accountsAdapter.add(AccountDetails.dummy()) accountsAdapter.add(AccountDetails.dummy())
} }
val officialKeyOnly = currentArguments.getBoolean(EXTRA_OFFICIAL_KEY_ONLY, false) val officialKeyOnly = arguments?.getBoolean(EXTRA_OFFICIAL_KEY_ONLY, false) ?: false
accountsAdapter.addAll(AccountUtils.getAllAccountDetails(AccountManager.get(currentContext), true).filter { accountsAdapter.addAll(AccountUtils.getAllAccountDetails(AccountManager.get(context), true).filter {
if (officialKeyOnly) { if (officialKeyOnly && !it.isOfficial(context)) {
return@filter false return@filter false
} }
return@filter conf.checkAccountAvailability(it) return@filter conf.checkAccountAvailability(it)

View File

@ -61,6 +61,7 @@ import org.mariotaku.ktextension.spannable
import org.mariotaku.library.objectcursor.ObjectCursor import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterUpload
import org.mariotaku.pickncrop.library.MediaPickerActivity import org.mariotaku.pickncrop.library.MediaPickerActivity
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
@ -90,6 +91,8 @@ import org.mariotaku.twidere.model.ParcelableMessageConversation.ConversationTyp
import org.mariotaku.twidere.model.ParcelableMessageConversation.ExtrasType import org.mariotaku.twidere.model.ParcelableMessageConversation.ExtrasType
import org.mariotaku.twidere.model.util.AccountUtils import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.task.twitter.UpdateStatusTask
import org.mariotaku.twidere.task.twitter.message.AddParticipantsTask
import org.mariotaku.twidere.task.twitter.message.ClearMessagesTask import org.mariotaku.twidere.task.twitter.message.ClearMessagesTask
import org.mariotaku.twidere.task.twitter.message.DestroyConversationTask import org.mariotaku.twidere.task.twitter.message.DestroyConversationTask
import org.mariotaku.twidere.task.twitter.message.SetConversationNotificationDisabledTask import org.mariotaku.twidere.task.twitter.message.SetConversationNotificationDisabledTask
@ -184,6 +187,12 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_CONVERSATION_ADD_USER -> {
if (resultCode == Activity.RESULT_OK && data != null) {
val user = data.getParcelableExtra<ParcelableUser>(EXTRA_USER)
performAddParticipant(user)
}
}
REQUEST_PICK_MEDIA -> { REQUEST_PICK_MEDIA -> {
when (resultCode) { when (resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
@ -318,6 +327,19 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
TaskStarter.execute(task) TaskStarter.execute(task)
} }
private fun performAddParticipant(user: ParcelableUser) {
ProgressDialogFragment.show(childFragmentManager, "add_participant_progress")
val weakThis = WeakReference(this)
val task = AddParticipantsTask(context!!, accountKey, conversationId, listOf(user))
task.callback = callback@ { succeed ->
val f = weakThis.get() ?: return@callback
f.dismissDialogThen("add_participant_progress") {
loaderManager.restartLoader(0, null, this)
}
}
TaskStarter.execute(task)
}
private fun performSetNotificationDisabled(disabled: Boolean) { private fun performSetNotificationDisabled(disabled: Boolean) {
ProgressDialogFragment.show(childFragmentManager, "set_notifications_disabled_progress") ProgressDialogFragment.show(childFragmentManager, "set_notifications_disabled_progress")
val weakThis = WeakReference(this) val weakThis = WeakReference(this)
@ -361,6 +383,9 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
val context = fragment.context val context = fragment.context
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return@updateAction microBlog.updateDmConversationName(conversationId, name).isSuccessful
}
} }
} }
throw UnsupportedOperationException() throw UnsupportedOperationException()
@ -370,6 +395,53 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
} }
private fun performSetConversationAvatar(uri: Uri?) { private fun performSetConversationAvatar(uri: Uri?) {
val conversationId = this.conversationId
performUpdateInfo("set_avatar_progress", updateAction = updateAction@ { fragment, account, microBlog ->
val context = fragment.context
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context) && context != null) {
val upload = account.newMicroBlogInstance(context, cls = TwitterUpload::class.java)
if (uri == null) {
val result = microBlog.updateDmConversationAvatar(conversationId, null)
if (result.isSuccessful) {
val dmResponse = microBlog.getDmConversation(conversationId, null).conversationTimeline
return@updateAction dmResponse.conversations[conversationId]?.avatarImageHttps
}
throw MicroBlogException("Error ${result.responseCode}")
}
var deleteAlways: List<UpdateStatusTask.MediaDeletionItem>? = null
try {
val media = arrayOf(ParcelableMediaUpdate().apply {
this.uri = uri.toString()
this.delete_always = true
})
val uploadResult = UpdateStatusTask.uploadMicroBlogMediaShared(context,
upload, account, media, null, null, true, null)
deleteAlways = uploadResult.deleteAlways
val avatarId = uploadResult.ids.first()
val result = microBlog.updateDmConversationAvatar(conversationId, avatarId)
if (result.isSuccessful) {
uploadResult.deleteOnSuccess.forEach { it.delete(context) }
val dmResponse = microBlog.getDmConversation(conversationId, null).conversationTimeline
return@updateAction dmResponse.conversations[conversationId]?.avatarImageHttps
}
throw MicroBlogException("Error ${result.responseCode}")
} catch (e: UpdateStatusTask.UploadException) {
e.deleteAlways?.forEach {
it.delete(context)
}
throw e
} finally {
deleteAlways?.forEach { it.delete(context) }
}
}
}
}
throw UnsupportedOperationException()
}, successAction = { uri ->
put(Conversations.CONVERSATION_AVATAR, uri)
})
} }
private inline fun <T> performUpdateInfo( private inline fun <T> performUpdateInfo(

View File

@ -44,6 +44,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.SelectableUsersAdapter import org.mariotaku.twidere.adapter.SelectableUsersAdapter
import org.mariotaku.twidere.constant.IntentConstants.* import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.nameFirstKey import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.queryOne import org.mariotaku.twidere.extension.queryOne
import org.mariotaku.twidere.extension.text.appendCompat import org.mariotaku.twidere.extension.text.appendCompat
import org.mariotaku.twidere.fragment.BaseFragment import org.mariotaku.twidere.fragment.BaseFragment
@ -242,7 +243,11 @@ class MessageNewConversationFragment : BaseFragment(), LoaderCallbacks<List<Parc
val activity = activity ?: return val activity = activity ?: return
val selected = this.selectedRecipients val selected = this.selectedRecipients
if (selected.isEmpty()) return if (selected.isEmpty()) return
val maxParticipants = 1 val maxParticipants = if (account.isOfficial(context)) {
defaultFeatures.twitterDirectMessageMaxParticipants
} else {
1
}
if (selected.size > maxParticipants) { if (selected.size > maxParticipants) {
editParticipants.error = getString(R.string.error_message_message_too_many_participants) editParticipants.error = getString(R.string.error_message_message_too_many_participants)
return return

View File

@ -0,0 +1,55 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment.status
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_STATUS
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.task.status.PinStatusTask
class PinStatusDialogFragment : AbsSimpleStatusOperationDialogFragment() {
override val title: String?
get() = getString(R.string.title_pin_status_confirm)
override val message: String
get() = getString(R.string.message_pin_status_confirm)
override fun onPerformAction(status: ParcelableStatus) {
val task = PinStatusTask(context!!, status.account_key, status.id)
TaskStarter.execute(task)
}
companion object {
val FRAGMENT_TAG = "pin_status"
fun show(fm: FragmentManager, status: ParcelableStatus): PinStatusDialogFragment {
val args = Bundle()
args.putParcelable(EXTRA_STATUS, status)
val f = PinStatusDialogFragment()
f.arguments = args
f.show(fm, FRAGMENT_TAG)
return f
}
}
}

View File

@ -123,6 +123,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
private lateinit var navigationHelper: RecyclerViewNavigationHelper private lateinit var navigationHelper: RecyclerViewNavigationHelper
private lateinit var scrollListener: RecyclerViewScrollHandler<StatusDetailsAdapter> private lateinit var scrollListener: RecyclerViewScrollHandler<StatusDetailsAdapter>
private var loadTranslationTask: LoadTranslationTask? = null
// Data fields // Data fields
private var conversationLoaderInitialized: Boolean = false private var conversationLoaderInitialized: Boolean = false
@ -505,9 +506,18 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
} }
internal fun loadTranslation(status: ParcelableStatus?) { internal fun loadTranslation(status: ParcelableStatus?) {
if (status == null) return
if (loadTranslationTask?.isFinished == true) return
loadTranslationTask = run {
val task = LoadTranslationTask(this, status)
TaskStarter.execute(task)
return@run task
}
} }
internal fun reloadTranslation() { internal fun reloadTranslation() {
loadTranslationTask = null
loadTranslation(adapter.status)
} }
private fun setConversation(data: List<ParcelableStatus>?) { private fun setConversation(data: List<ParcelableStatus>?) {
@ -673,6 +683,37 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
} }
} }
internal class LoadTranslationTask(fragment: StatusFragment, val status: ParcelableStatus) :
AbsAccountRequestTask<Any?, TranslationResult, Any?>(fragment.context!!, status.account_key) {
private val weakFragment = WeakReference(fragment)
override fun onExecute(account: AccountDetails, params: Any?): TranslationResult {
val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java)
val prefDest = preferences.getString(KEY_TRANSLATION_DESTINATION, null).orEmpty()
val dest: String
if (TextUtils.isEmpty(prefDest)) {
dest = twitter.accountSettings.language
val editor = preferences.edit()
editor.putString(KEY_TRANSLATION_DESTINATION, dest)
editor.apply()
} else {
dest = prefDest
}
return twitter.showTranslation(status.originalId, dest)
}
override fun onSucceed(callback: Any?, result: TranslationResult) {
val fragment = weakFragment.get() ?: return
fragment.displayTranslation(result)
}
override fun onException(callback: Any?, exception: MicroBlogException) {
Toast.makeText(context, exception.getErrorMessage(context), Toast.LENGTH_SHORT).show()
}
}
class StatusActivitySummaryLoader( class StatusActivitySummaryLoader(
context: Context, context: Context,
private val accountKey: UserKey, private val accountKey: UserKey,

View File

@ -0,0 +1,55 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment.status
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.twidere.R
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_STATUS
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.task.status.UnpinStatusTask
class UnpinStatusDialogFragment : AbsSimpleStatusOperationDialogFragment() {
override val title: String?
get() = getString(R.string.title_unpin_status_confirm)
override val message: String
get() = getString(R.string.message_unpin_status_confirm)
override fun onPerformAction(status: ParcelableStatus) {
val task = UnpinStatusTask(context!!, status.account_key, status.id)
TaskStarter.execute(task)
}
companion object {
val FRAGMENT_TAG = "unpin_status"
fun show(fm: FragmentManager, status: ParcelableStatus): UnpinStatusDialogFragment {
val args = Bundle()
args.putParcelable(EXTRA_STATUS, status)
val f = UnpinStatusDialogFragment()
f.arguments = args
f.show(fm, FRAGMENT_TAG)
return f
}
}
}

View File

@ -32,6 +32,7 @@ import org.mariotaku.twidere.loader.users.AbsRequestUsersLoader
import org.mariotaku.twidere.loader.users.IncomingFriendshipsLoader import org.mariotaku.twidere.loader.users.IncomingFriendshipsLoader
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.FriendshipTaskEvent import org.mariotaku.twidere.model.event.FriendshipTaskEvent
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.view.holder.UserViewHolder import org.mariotaku.twidere.view.holder.UserViewHolder
class IncomingFriendshipsFragment : ParcelableUsersFragment(), IUsersAdapter.RequestClickListener { class IncomingFriendshipsFragment : ParcelableUsersFragment(), IUsersAdapter.RequestClickListener {
@ -48,6 +49,8 @@ class IncomingFriendshipsFragment : ParcelableUsersFragment(), IUsersAdapter.Req
val accountKey = arguments?.getParcelable<UserKey?>(EXTRA_ACCOUNT_KEY) ?: return adapter val accountKey = arguments?.getParcelable<UserKey?>(EXTRA_ACCOUNT_KEY) ?: return adapter
if (USER_TYPE_FANFOU_COM == accountKey.host) { if (USER_TYPE_FANFOU_COM == accountKey.host) {
adapter.requestClickListener = this adapter.requestClickListener = this
} else if (AccountUtils.isOfficial(context, accountKey)) {
adapter.requestClickListener = this
} }
return adapter return adapter
} }

View File

@ -62,6 +62,6 @@ class DefaultAPIConfigLoader(context: Context) : FixedAsyncTaskLoader<List<Custo
} }
companion object { companion object {
const val DEFAULT_API_CONFIGS_URL = "https://twidere.mariotaku.org/assets/data/default_api_configs.json" const val DEFAULT_API_CONFIGS_URL = "https://twidereproject.github.io/default_api_configs.json"
} }
} }

View File

@ -38,6 +38,7 @@ import org.mariotaku.twidere.extension.atto.filter
import org.mariotaku.twidere.extension.atto.firstElementOrNull import org.mariotaku.twidere.extension.atto.firstElementOrNull
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.makeOriginal import org.mariotaku.twidere.extension.model.makeOriginal
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
@ -92,6 +93,11 @@ class ConversationLoader(
// val isOfficial = account.isOfficial(context) // val isOfficial = account.isOfficial(context)
val isOfficial = false val isOfficial = false
canLoadAllReplies = isOfficial canLoadAllReplies = isOfficial
if (isOfficial) {
return microBlog.showConversation(status.id, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
}
return showConversationCompat(microBlog, account, status, true) return showConversationCompat(microBlog, account, status, true)
} }
AccountType.STATUSNET -> { AccountType.STATUSNET -> {

View File

@ -27,10 +27,12 @@ import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.SearchQuery import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.UniversalSearchQuery
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.official
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
@ -76,6 +78,9 @@ open class MediaStatusesSearchLoader(
protected open fun processQuery(details: AccountDetails, query: String): String { protected open fun processQuery(details: AccountDetails, query: String): String {
if (details.type == AccountType.TWITTER) { if (details.type == AccountType.TWITTER) {
if (details.extras?.official == true) {
return TweetSearchLoader.smQuery("$query filter:media", pagination)
}
return "$query filter:media exclude:retweets" return "$query filter:media exclude:retweets"
} }
return query return query
@ -87,6 +92,15 @@ open class MediaStatusesSearchLoader(
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java) val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
if (account.extras?.official == true) {
val universalQuery = UniversalSearchQuery(queryText)
universalQuery.setModules(UniversalSearchQuery.Module.TWEET)
universalQuery.setResultType(UniversalSearchQuery.ResultType.RECENT)
universalQuery.setPaging(paging)
val searchResult = microBlog.universalSearch(universalQuery)
return searchResult.modules.mapNotNull { it.status?.data }
}
val searchQuery = SearchQuery(queryText) val searchQuery = SearchQuery(queryText)
searchQuery.paging(paging) searchQuery.paging(paging)
return microBlog.search(searchQuery) return microBlog.search(searchQuery)

View File

@ -34,6 +34,7 @@ import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.api.updateFilterInfoForUserTimeline import org.mariotaku.twidere.extension.model.api.updateFilterInfoForUserTimeline
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.ParcelableStatus
@ -89,23 +90,32 @@ class MediaTimelineLoader(
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java) val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
val screenName = this.screenName ?: run { if (account.isOfficial(context)) {
return@run this.user ?: run fetchUser@ { if (userKey != null) {
if (userKey == null) throw MicroBlogException("Invalid parameters") return microBlog.getMediaTimeline(userKey.id, paging)
val user = microBlog.tryShowUser(userKey.id, null, account.type) }
this.user = user if (screenName != null) {
return@fetchUser user return microBlog.getMediaTimelineByScreenName(screenName, paging)
}.screenName }
} else {
val screenName = this.screenName ?: run {
return@run this.user ?: run fetchUser@ {
if (userKey == null) throw MicroBlogException("Invalid parameters")
val user = microBlog.tryShowUser(userKey.id, null, account.type)
this.user = user
return@fetchUser user
}.screenName
}
val query = SearchQuery("from:$screenName filter:media exclude:retweets")
query.paging(paging)
val result = ResponseList<Status>()
microBlog.search(query).filterTo(result) { status ->
val user = status.user
return@filterTo user.id == userKey?.id
|| user.screenName.equals(this.screenName, ignoreCase = true)
}
return result
} }
val query = SearchQuery("from:$screenName filter:media exclude:retweets")
query.paging(paging)
val result = ResponseList<Status>()
microBlog.search(query).filterTo(result) { status ->
val user = status.user
return@filterTo user.id == userKey?.id
|| user.screenName.equals(this.screenName, ignoreCase = true)
}
return result
throw MicroBlogException("Wrong user") throw MicroBlogException("Wrong user")
} }
AccountType.FANFOU -> { AccountType.FANFOU -> {

View File

@ -34,6 +34,7 @@ import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.official
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
@ -75,6 +76,9 @@ open class TweetSearchLoader(
protected open fun processQuery(details: AccountDetails, query: String): String { protected open fun processQuery(details: AccountDetails, query: String): String {
if (details.type == AccountType.TWITTER) { if (details.type == AccountType.TWITTER) {
if (details.extras?.official == true) {
return smQuery(query, pagination)
}
return "$query exclude:retweets" return "$query exclude:retweets"
} }
return query return query
@ -104,6 +108,15 @@ open class TweetSearchLoader(
val queryText = processQuery(account, query) val queryText = processQuery(account, query)
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
if (account.extras?.official == true) {
val universalQuery = UniversalSearchQuery(queryText)
universalQuery.setModules(UniversalSearchQuery.Module.TWEET)
universalQuery.setResultType(UniversalSearchQuery.ResultType.RECENT)
universalQuery.setPaging(paging)
val searchResult = microBlog.universalSearch(universalQuery)
return searchResult.modules.mapNotNull { it.status?.data }
}
val searchQuery = SearchQuery(queryText) val searchQuery = SearchQuery(queryText)
searchQuery.paging(paging) searchQuery.paging(paging)
return microBlog.search(searchQuery) return microBlog.search(searchQuery)

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.loader.statuses
import android.content.Context import android.content.Context
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.official
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
@ -42,6 +43,9 @@ class UserMentionsLoader(
override fun processQuery(details: AccountDetails, query: String): String { override fun processQuery(details: AccountDetails, query: String): String {
val screenName = query.substringAfter("@") val screenName = query.substringAfter("@")
if (details.type == AccountType.TWITTER) { if (details.type == AccountType.TWITTER) {
if (details.extras?.official == true) {
return smQuery("to:$screenName", pagination)
}
return "to:$screenName exclude:retweets" return "to:$screenName exclude:retweets"
} }
return "@$screenName -RT" return "@$screenName -RT"

View File

@ -36,6 +36,7 @@ import org.mariotaku.twidere.extension.api.lookupUsersMapPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.ParcelableUser
@ -62,7 +63,9 @@ class StatusFavoritersLoader(
} }
AccountType.TWITTER -> { AccountType.TWITTER -> {
val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java) val microBlog = details.newMicroBlogInstance(context, MicroBlog::class.java)
val ids = run { val ids = if (details.isOfficial(context)) {
microBlog.getStatusActivitySummary(statusId).favoriters
} else {
val web = details.newMicroBlogInstance(context, TwitterWeb::class.java) val web = details.newMicroBlogInstance(context, TwitterWeb::class.java)
val htmlUsers = web.getFavoritedPopup(statusId).htmlUsers val htmlUsers = web.getFavoritedPopup(statusId).htmlUsers
IDsAccessor.setIds(IDs(), parseUserIds(htmlUsers)) IDsAccessor.setIds(IDs(), parseUserIds(htmlUsers))

View File

@ -29,6 +29,7 @@ import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.TabAccountFlags import org.mariotaku.twidere.annotation.TabAccountFlags
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_MENTIONS_ONLY import org.mariotaku.twidere.constant.IntentConstants.EXTRA_MENTIONS_ONLY
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_MY_FOLLOWING_ONLY import org.mariotaku.twidere.constant.IntentConstants.EXTRA_MY_FOLLOWING_ONLY
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.fragment.InteractionsTimelineFragment import org.mariotaku.twidere.fragment.InteractionsTimelineFragment
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.Tab import org.mariotaku.twidere.model.Tab
@ -104,11 +105,11 @@ class InteractionsTabConfiguration : TabConfiguration() {
val am = AccountManager.get(context) val am = AccountManager.get(context)
val accounts = AccountUtils.getAllAccountDetails(am, false) val accounts = AccountUtils.getAllAccountDetails(am, false)
interactionsAvailable = accounts.any { it.supportsInteractions } interactionsAvailable = accounts.any { it.supportsInteractions }
requiresStreaming = accounts.all { true } requiresStreaming = accounts.all { it.requiresStreaming }
} else when (account.type) { } else when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
interactionsAvailable = true interactionsAvailable = true
requiresStreaming = true requiresStreaming = !account.isOfficial(context)
} }
AccountType.MASTODON -> { AccountType.MASTODON -> {
interactionsAvailable = true interactionsAvailable = true
@ -160,7 +161,7 @@ class InteractionsTabConfiguration : TabConfiguration() {
get() = type == AccountType.TWITTER || type == AccountType.MASTODON get() = type == AccountType.TWITTER || type == AccountType.MASTODON
private val AccountDetails.requiresStreaming: Boolean private val AccountDetails.requiresStreaming: Boolean
get() = true get() = !isOfficial(context)
} }
} }

View File

@ -194,6 +194,11 @@ class StreamingService : BaseService() {
} }
private fun newStreamingRunnable(account: AccountDetails, preferences: AccountPreferences): StreamingRunnable<*>? { private fun newStreamingRunnable(account: AccountDetails, preferences: AccountPreferences): StreamingRunnable<*>? {
when (account.type) {
AccountType.TWITTER -> {
return TwitterStreamingRunnable(this, account, preferences)
}
}
return null return null
} }
@ -232,6 +237,236 @@ class StreamingService : BaseService() {
abstract fun onCancelled() abstract fun onCancelled()
} }
internal inner class TwitterStreamingRunnable(
context: Context,
account: AccountDetails,
accountPreferences: AccountPreferences
) : StreamingRunnable<TwitterUserStream>(context, account, accountPreferences) {
private val profileImageSize = context.getString(R.string.profile_image_size)
private val isOfficial = account.isOfficial(context)
private var canGetInteractions: Boolean = true
private var canGetMessages: Boolean = true
private val interactionsTimeoutRunnable = Runnable {
canGetInteractions = true
}
private val messagesTimeoutRunnable = Runnable {
canGetMessages = true
}
val callback = object : TwitterTimelineStreamCallback(account.key.id) {
private var lastStatusTimestamps = LongArray(2)
private var homeInsertGap = false
private var interactionsInsertGap = false
private var lastActivityAboutMe: ParcelableActivity? = null
override fun onConnected(): Boolean {
homeInsertGap = true
interactionsInsertGap = true
return true
}
override fun onHomeTimeline(status: Status): Boolean {
if (!accountPreferences.isStreamHomeTimelineEnabled) {
homeInsertGap = true
return false
}
val parcelableStatus = status.toParcelable(account, profileImageSize = profileImageSize)
parcelableStatus.is_gap = homeInsertGap
val currentTimeMillis = System.currentTimeMillis()
if (lastStatusTimestamps[0] >= parcelableStatus.timestamp) {
val extraValue = (currentTimeMillis - lastStatusTimestamps[1]).coerceAtMost(499)
parcelableStatus.position_key = parcelableStatus.timestamp + extraValue
} else {
parcelableStatus.position_key = parcelableStatus.timestamp
}
parcelableStatus.inserted_date = currentTimeMillis
lastStatusTimestamps[0] = parcelableStatus.position_key
lastStatusTimestamps[1] = parcelableStatus.inserted_date
val values = ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java)
.create(parcelableStatus)
context.contentResolver.insert(Statuses.CONTENT_URI, values)
homeInsertGap = false
return true
}
override fun onActivityAboutMe(activity: Activity): Boolean {
if (!accountPreferences.isStreamInteractionsEnabled) {
interactionsInsertGap = true
return false
}
if (isOfficial) {
// Wait for 30 seconds to avoid rate limit
if (canGetInteractions) {
handler.post { getInteractions() }
canGetInteractions = false
handler.postDelayed(interactionsTimeoutRunnable, TimeUnit.SECONDS.toMillis(30))
}
} else {
val insertGap: Boolean
if (activity.action in Activity.Action.MENTION_ACTIONS) {
insertGap = interactionsInsertGap
interactionsInsertGap = false
} else {
insertGap = false
}
val curActivity = activity.toParcelable(account, insertGap, profileImageSize)
curActivity.account_color = account.color
curActivity.position_key = curActivity.timestamp
var updateId = -1L
if (curActivity.action !in Activity.Action.MENTION_ACTIONS) {
/* Merge two activities if:
* * Not mention/reply/quote
* * Same action
* * Same source or target or target object
*/
val lastActivity = this.lastActivityAboutMe
if (lastActivity != null && curActivity.action == lastActivity.action) {
if (curActivity.reachedCountLimit) {
// Skip if more than 10 sources/targets/target_objects
} else if (curActivity.isSameSources(lastActivity)) {
curActivity.prependTargets(lastActivity)
curActivity.prependTargetObjects(lastActivity)
updateId = lastActivity._id
} else if (curActivity.isSameTarget(lastActivity)) {
curActivity.prependSources(lastActivity)
curActivity.prependTargets(lastActivity)
updateId = lastActivity._id
} else if (curActivity.isSameTargetObject(lastActivity)) {
curActivity.prependSources(lastActivity)
curActivity.prependTargets(lastActivity)
updateId = lastActivity._id
}
if (updateId > 0) {
curActivity.min_position = lastActivity.min_position
curActivity.min_sort_position = lastActivity.min_sort_position
}
}
}
val values = ObjectCursor.valuesCreatorFrom(ParcelableActivity::class.java)
.create(curActivity)
val resolver = context.contentResolver
if (updateId > 0) {
val where = Expression.equals(Activities._ID, updateId).sql
resolver.update(Activities.AboutMe.CONTENT_URI, values, where, null)
curActivity._id = updateId
} else {
val uri = resolver.insert(Activities.AboutMe.CONTENT_URI, values)
if (uri != null) {
curActivity._id = uri.lastPathSegment.toLongOr(-1L)
}
}
lastActivityAboutMe = curActivity
}
return true
}
@WorkerThread
override fun onDirectMessage(directMessage: DirectMessage): Boolean {
if (!accountPreferences.isStreamDirectMessagesEnabled) {
return false
}
if (canGetMessages) {
handler.post { getMessages() }
canGetMessages = false
val timeout = TimeUnit.SECONDS.toMillis(if (isOfficial) 30 else 90)
handler.postDelayed(messagesTimeoutRunnable, timeout)
}
return true
}
override fun onAllStatus(status: Status) {
if (!accountPreferences.isStreamNotificationUsersEnabled) {
return
}
val user = status.user ?: return
val userKey = user.key
val where = Expression.and(Expression.equalsArgs(CachedRelationships.ACCOUNT_KEY),
Expression.equalsArgs(CachedRelationships.USER_KEY),
Expression.equals(CachedRelationships.NOTIFICATIONS_ENABLED, 1)).sql
val whereArgs = arrayOf(account.key.toString(), userKey.toString())
if (context.contentResolver.queryCount(CachedRelationships.CONTENT_URI,
where, whereArgs) <= 0) return
contentNotificationManager.showUserNotification(account.key, status, userKey)
}
override fun onStatusDeleted(event: DeletionEvent): Boolean {
val deleteWhere = Expression.and(Expression.likeRaw(Columns.Column(Statuses.ACCOUNT_KEY), "'%@'||?"),
Expression.equalsArgs(Columns.Column(Statuses.ID))).sql
val deleteWhereArgs = arrayOf(account.key.host, event.id)
context.contentResolver.delete(Statuses.CONTENT_URI, deleteWhere, deleteWhereArgs)
return true
}
override fun onDisconnectNotice(code: Int, reason: String?): Boolean {
disconnect()
return true
}
override fun onException(ex: Throwable): Boolean {
DebugLog.w(LOGTAG, msg = "Exception for ${account.key}", tr = ex)
return true
}
override fun onUnhandledEvent(obj: TwitterStreamObject, json: String) {
DebugLog.d(LOGTAG, msg = "Unhandled event ${obj.determine()} for ${account.key}: $json")
}
@UiThread
private fun getInteractions() {
val task = GetActivitiesAboutMeTask(context)
task.params = object : RefreshTaskParam {
override val accountKeys: Array<UserKey> = arrayOf(account.key)
override val pagination by lazy {
val keys = accountKeys.toNulls()
val sinceIds = DataStoreUtils.getRefreshNewestActivityMaxPositions(context,
Activities.AboutMe.CONTENT_URI, keys)
val sinceSortIds = DataStoreUtils.getRefreshNewestActivityMaxSortPositions(context,
Activities.AboutMe.CONTENT_URI, keys)
return@lazy Array(keys.size) { idx ->
SinceMaxPagination.sinceId(sinceIds[idx], sinceSortIds[idx])
}
}
}
TaskStarter.execute(task)
}
@UiThread
private fun getMessages() {
val task = GetMessagesTask(context)
task.params = object : GetMessagesTask.RefreshMessagesTaskParam(context) {
override val accountKeys: Array<UserKey> = arrayOf(account.key)
}
TaskStarter.execute(task)
}
}
override fun createStreamingInstance(): TwitterUserStream {
return account.newMicroBlogInstance(context, cls = TwitterUserStream::class.java)
}
override fun TwitterUserStream.beginStreaming() {
getUserStream(StreamWith.USER, callback)
}
override fun onCancelled() {
callback.disconnect()
}
}
companion object { companion object {
private val NOTIFICATION_SERVICE_STARTED = 1 private val NOTIFICATION_SERVICE_STARTED = 1

View File

@ -15,7 +15,6 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.event.FriendshipTaskEvent import org.mariotaku.twidere.model.event.FriendshipTaskEvent
import org.mariotaku.twidere.util.Utils import org.mariotaku.twidere.util.Utils
import java.lang.UnsupportedOperationException
/** /**
* Created by mariotaku on 16/3/11. * Created by mariotaku on 16/3/11.
@ -36,7 +35,9 @@ class AcceptFriendshipTask(context: Context) : AbsFriendshipOperationTask(contex
return mastodon.getAccount(args.userKey.id).toParcelable(details) return mastodon.getAccount(args.userKey.id).toParcelable(details)
} }
else -> { else -> {
throw UnsupportedOperationException() val twitter = details.newMicroBlogInstance(context, MicroBlog::class.java)
return twitter.acceptFriendship(args.userKey.id).toParcelable(details,
profileImageSize = profileImageSize)
} }
} }
} }

View File

@ -15,7 +15,6 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.event.FriendshipTaskEvent import org.mariotaku.twidere.model.event.FriendshipTaskEvent
import org.mariotaku.twidere.util.Utils import org.mariotaku.twidere.util.Utils
import java.lang.UnsupportedOperationException
/** /**
* Created by mariotaku on 16/3/11. * Created by mariotaku on 16/3/11.
@ -36,7 +35,9 @@ class DenyFriendshipTask(context: Context) : AbsFriendshipOperationTask(context,
return mastodon.getAccount(args.userKey.id).toParcelable(details) return mastodon.getAccount(args.userKey.id).toParcelable(details)
} }
else -> { else -> {
throw UnsupportedOperationException() val twitter = details.newMicroBlogInstance(context, MicroBlog::class.java)
return twitter.denyFriendship(args.userKey.id).toParcelable(details,
profileImageSize = profileImageSize)
} }
} }
} }

View File

@ -20,9 +20,15 @@
package org.mariotaku.twidere.task package org.mariotaku.twidere.task
import android.app.Activity import android.app.Activity
import android.content.ContentValues
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast import android.widget.Toast
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.CacheFileType
import org.mariotaku.twidere.provider.CacheProvider
import java.io.File import java.io.File
/** /**
@ -30,14 +36,53 @@ import java.io.File
*/ */
class SaveMediaToGalleryTask( class SaveMediaToGalleryTask(
activity: Activity, activity: Activity,
fileInfo: FileInfo, private val fileInfo: FileInfo,
destination: File destination: File
) : ProgressSaveFileTask(activity, destination, fileInfo) { ) : ProgressSaveFileTask(activity, destination, fileInfo) {
override fun onFileSaved(savedFile: File, mimeType: String?) { override fun onFileSaved(savedFile: File, mimeType: String?) {
val context = context ?: return val context = context ?: return
MediaScannerConnection.scanFile(context, arrayOf(savedFile.path), MediaScannerConnection.scanFile(context, arrayOf(savedFile.path),
arrayOf(mimeType), null) arrayOf(mimeType), null)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val type = (fileInfo as? CacheProvider.CacheFileTypeSupport)?.cacheFileType
val path = when (type) {
CacheFileType.VIDEO -> {
Environment.DIRECTORY_MOVIES
}
CacheFileType.IMAGE -> {
Environment.DIRECTORY_PICTURES
}
else -> {
Environment.DIRECTORY_DOWNLOADS
}
}
val url = when (type) {
CacheFileType.VIDEO -> {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
CacheFileType.IMAGE -> {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
else -> {
MediaStore.Downloads.EXTERNAL_CONTENT_URI
}
}
val contentValues = ContentValues()
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileInfo.fileName)
contentValues.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
contentValues.put(MediaStore.Images.Media.MIME_TYPE, fileInfo.mimeType)
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "$path/Twidere")
context.contentResolver.insert(url, contentValues)?.let { uri ->
context.contentResolver.openOutputStream(uri)?.use {
savedFile.inputStream().use { fileInputStream ->
fileInputStream.copyTo(it)
}
}
}
}
savedFile.delete()
Toast.makeText(context, R.string.message_toast_saved_to_gallery, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.message_toast_saved_to_gallery, Toast.LENGTH_SHORT).show()
} }

View File

@ -0,0 +1,52 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.task.status
import android.content.Context
import android.widget.Toast
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.twitter.model.PinTweetResult
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.StatusPinEvent
import org.mariotaku.twidere.task.AbsAccountRequestTask
/**
* Created by mariotaku on 2017/4/28.
*/
class PinStatusTask(context: Context, accountKey: UserKey, val id: String) : AbsAccountRequestTask<Any?,
PinTweetResult, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): PinTweetResult {
val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java)
return twitter.pinTweet(id)
}
override fun onSucceed(callback: Any?, result: PinTweetResult) {
super.onSucceed(callback, result)
Toast.makeText(context, R.string.message_toast_status_pinned, Toast.LENGTH_SHORT).show()
if (accountKey != null) {
bus.post(StatusPinEvent(accountKey, true))
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.task.status
import android.content.Context
import android.widget.Toast
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.twitter.model.PinTweetResult
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.StatusPinEvent
import org.mariotaku.twidere.task.AbsAccountRequestTask
/**
* Created by mariotaku on 2017/4/28.
*/
class UnpinStatusTask(context: Context, accountKey: UserKey, val id: String) : AbsAccountRequestTask<Any?,
PinTweetResult, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): PinTweetResult {
val twitter = account.newMicroBlogInstance(context, MicroBlog::class.java)
return twitter.unpinTweet(id)
}
override fun onSucceed(callback: Any?, result: PinTweetResult) {
super.onSucceed(callback, result)
Toast.makeText(context, R.string.message_toast_status_unpinned, Toast.LENGTH_SHORT).show()
if (accountKey != null) {
bus.post(StatusPinEvent(accountKey, false))
}
}
}

View File

@ -36,6 +36,7 @@ import org.mariotaku.twidere.extension.api.batchGetRelationships
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.microblog.toParcelable import org.mariotaku.twidere.extension.model.api.microblog.toParcelable
import org.mariotaku.twidere.extension.model.extractFanfouHashtags import org.mariotaku.twidere.extension.model.extractFanfouHashtags
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.fragment.InteractionsTimelineFragment import org.mariotaku.twidere.fragment.InteractionsTimelineFragment
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
@ -81,6 +82,28 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
notification.status?.tags?.map { it.name }.orEmpty() notification.status?.tags?.map { it.name }.orEmpty()
}) })
} }
AccountType.TWITTER -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
if (account.isOfficial(context)) {
val timeline = microBlog.getActivitiesAboutMe(paging)
val activities = timeline.map {
it.toParcelable(account, profileImageSize = profileImageSize)
}
return GetTimelineResult(account, activities, activities.flatMap {
it.sources?.toList().orEmpty()
}, timeline.flatMapTo(HashSet()) { activity ->
val mapResult = mutableSetOf<String>()
activity.targetStatuses?.flatMapTo(mapResult) { status ->
status.entities?.hashtags?.map { it.text }.orEmpty()
}
activity.targetObjectStatuses?.flatMapTo(mapResult) { status ->
status.entities?.hashtags?.map { it.text }.orEmpty()
}
return@flatMapTo mapResult
})
}
}
AccountType.FANFOU -> { AccountType.FANFOU -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java) val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val activities = microBlog.getMentions(paging).map { val activities = microBlog.getMentions(paging).map {

View File

@ -181,8 +181,10 @@ abstract class GetActivitiesTask(
valuesList[valuesList.size - 1].put(Activities.IS_GAP, true) valuesList[valuesList.size - 1].put(Activities.IS_GAP, true)
} }
} }
// Insert previously fetched items. if (valuesList.isNotEmpty()) {
ContentResolverUtils.bulkInsert(cr, writeUri, valuesList) // Insert previously fetched items.
ContentResolverUtils.bulkInsert(cr, writeUri, valuesList)
}
// Remove gap flag // Remove gap flag
if (maxId != null && sinceId == null) { if (maxId != null && sinceId == null) {

View File

@ -244,7 +244,7 @@ abstract class GetStatusesTask(
if (result == null) return@forEach if (result == null) return@forEach
val account = result.account val account = result.account
val task = CacheTimelineResultTask(context, result, val task = CacheTimelineResultTask(context, result,
account.type == AccountType.STATUSNET) account.type == AccountType.STATUSNET || account.isOfficial(context))
TaskStarter.execute(task) TaskStarter.execute(task)
} }
} }

View File

@ -0,0 +1,97 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.task.twitter.message
import android.accounts.AccountManager
import android.content.Context
import org.mariotaku.ktextension.mapToArray
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.addParticipants
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.task.ExceptionHandlingAbstractTask
import org.mariotaku.twidere.util.DataStoreUtils
/**
* Created by mariotaku on 2017/2/25.
*/
class AddParticipantsTask(
context: Context,
val accountKey: UserKey,
val conversationId: String,
val participants: Collection<ParcelableUser>
) : ExceptionHandlingAbstractTask<Unit?, Boolean, MicroBlogException, ((Boolean) -> Unit)?>(context) {
private val profileImageSize: String = context.getString(R.string.profile_image_size)
override val exceptionClass = MicroBlogException::class.java
override fun onExecute(params: Unit?): Boolean {
val account = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey, true) ?:
throw MicroBlogException("No account")
val conversation = DataStoreUtils.findMessageConversation(context, accountKey, conversationId)
if (conversation != null && conversation.is_temp) {
val addData = GetMessagesTask.DatabaseUpdateData(listOf(conversation), emptyList())
conversation.addParticipants(participants)
GetMessagesTask.storeMessages(context, addData, account, showNotification = false)
// Don't finish too fast
Thread.sleep(300L)
return true
}
val microBlog = account.newMicroBlogInstance(context, cls = MicroBlog::class.java)
val addData = requestAddParticipants(microBlog, account, conversation)
GetMessagesTask.storeMessages(context, addData, account, showNotification = false)
return true
}
override fun afterExecute(callback: ((Boolean) -> Unit)?, result: Boolean?, exception: MicroBlogException?) {
callback?.invoke(result ?: false)
}
private fun requestAddParticipants(microBlog: MicroBlog, account: AccountDetails, conversation: ParcelableMessageConversation?):
GetMessagesTask.DatabaseUpdateData {
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
val ids = participants.mapToArray { it.key.id }
val response = microBlog.addParticipants(conversationId, ids)
if (conversation != null) {
conversation.addParticipants(participants)
return GetMessagesTask.DatabaseUpdateData(listOf(conversation), emptyList())
}
return GetMessagesTask.createDatabaseUpdateData(context, account, response,
profileImageSize)
}
}
}
throw MicroBlogException("Adding participants is not supported")
}
}

View File

@ -25,6 +25,7 @@ import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableMessageConversation import org.mariotaku.twidere.model.ParcelableMessageConversation
@ -88,6 +89,9 @@ class DestroyConversationTask(
private fun requestDestroyConversation(microBlog: MicroBlog, account: AccountDetails): Boolean { private fun requestDestroyConversation(microBlog: MicroBlog, account: AccountDetails): Boolean {
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return microBlog.deleteDmConversation(conversationId).isSuccessful
}
} }
} }
return false return false

View File

@ -25,6 +25,7 @@ import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.UserKey import org.mariotaku.twidere.model.UserKey
@ -74,6 +75,9 @@ class DestroyMessageTask(
account: AccountDetails, messageId: String): Boolean { account: AccountDetails, messageId: String): Boolean {
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return microBlog.destroyDm(messageId).isSuccessful
}
} }
} }
microBlog.destroyDirectMessage(messageId) microBlog.destroyDirectMessage(messageId)

View File

@ -104,11 +104,27 @@ class GetMessagesTask(
// Use fanfou DM api, disabled since it's conversation api is not suitable for paging // Use fanfou DM api, disabled since it's conversation api is not suitable for paging
// return getFanfouMessages(microBlog, details, param, index) // return getFanfouMessages(microBlog, details, param, index)
} }
AccountType.TWITTER -> {
// Use official DM api
if (details.isOfficial(context)) {
return getTwitterOfficialMessages(microBlog, details, param, index)
}
}
} }
// Use default method // Use default method
return getDefaultMessages(microBlog, details, param, index) return getDefaultMessages(microBlog, details, param, index)
} }
private fun getTwitterOfficialMessages(microBlog: MicroBlog, details: AccountDetails,
param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val conversationId = param.conversationId
if (conversationId == null) {
return getTwitterOfficialUserInbox(microBlog, details, param, index)
} else {
return getTwitterOfficialConversation(microBlog, details, conversationId, param, index)
}
}
private fun getFanfouMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { private fun getFanfouMessages(microBlog: MicroBlog, details: AccountDetails, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val conversationId = param.conversationId val conversationId = param.conversationId
if (conversationId == null) { if (conversationId == null) {
@ -169,6 +185,35 @@ class GetMessagesTask(
} }
private fun getTwitterOfficialConversation(microBlog: MicroBlog, details: AccountDetails,
conversationId: String, param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val maxId = (param.pagination?.get(index) as? SinceMaxPagination)?.maxId
?: return DatabaseUpdateData(emptyList(), emptyList())
val paging = Paging().apply {
maxId(maxId)
}
val response = microBlog.getDmConversation(conversationId, paging).conversationTimeline
return createDatabaseUpdateData(context, details, response, profileImageSize)
}
private fun getTwitterOfficialUserInbox(microBlog: MicroBlog, details: AccountDetails,
param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val maxId = (param.pagination?.get(index) as? SinceMaxPagination)?.maxId
val cursor = (param.pagination?.get(index) as? CursorPagination)?.cursor
val response = if (cursor != null) {
microBlog.getUserUpdates(cursor).userEvents
} else {
microBlog.getUserInbox(Paging().apply {
if (maxId != null) {
maxId(maxId)
}
}).userInbox
} ?: throw MicroBlogException("Null response data")
return createDatabaseUpdateData(context, details, response, profileImageSize)
}
private fun getFanfouConversations(microBlog: MicroBlog, details: AccountDetails, private fun getFanfouConversations(microBlog: MicroBlog, details: AccountDetails,
param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData { param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val accountKey = details.key val accountKey = details.key
@ -220,10 +265,16 @@ class GetMessagesTask(
defaultKeys, false) defaultKeys, false)
val outgoingIds = DataStoreUtils.getNewestMessageIds(context, Messages.CONTENT_URI, val outgoingIds = DataStoreUtils.getNewestMessageIds(context, Messages.CONTENT_URI,
defaultKeys, true) defaultKeys, true)
val cursors = DataStoreUtils.getNewestConversations(context, Conversations.CONTENT_URI,
twitterOfficialKeys).mapToArray { it?.request_cursor }
accounts.forEachIndexed { index, details -> accounts.forEachIndexed { index, details ->
if (details == null) return@forEachIndexed if (details == null) return@forEachIndexed
result[index] = SinceMaxPagination.sinceId(incomingIds[index], -1) if (details.isOfficial(context)) {
result[accounts.size + index] = SinceMaxPagination.sinceId(outgoingIds[index], -1) result[index] = CursorPagination.valueOf(cursors[index])
} else {
result[index] = SinceMaxPagination.sinceId(incomingIds[index], -1)
result[accounts.size + index] = SinceMaxPagination.sinceId(outgoingIds[index], -1)
}
} }
return@lazy result return@lazy result
} }
@ -240,7 +291,7 @@ class GetMessagesTask(
val outgoingIds = DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI, val outgoingIds = DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI,
defaultKeys, true) defaultKeys, true)
val oldestConversations = DataStoreUtils.getOldestConversations(context, val oldestConversations = DataStoreUtils.getOldestConversations(context,
Conversations.CONTENT_URI, emptyArray()) Conversations.CONTENT_URI, twitterOfficialKeys)
oldestConversations.forEachIndexed { i, conversation -> oldestConversations.forEachIndexed { i, conversation ->
val extras = conversation?.conversation_extras as? TwitterOfficialConversationExtras ?: return@forEachIndexed val extras = conversation?.conversation_extras as? TwitterOfficialConversationExtras ?: return@forEachIndexed
incomingIds[i] = extras.maxEntryId incomingIds[i] = extras.maxEntryId
@ -281,6 +332,19 @@ class GetMessagesTask(
protected val defaultKeys: Array<UserKey?> by lazy { protected val defaultKeys: Array<UserKey?> by lazy {
return@lazy accounts.map { account -> return@lazy accounts.map { account ->
account ?: return@map null account ?: return@map null
if (account.isOfficial(context)) {
return@map null
}
return@map account.key
}.toTypedArray()
}
protected val twitterOfficialKeys: Array<UserKey?> by lazy {
return@lazy accounts.map { account ->
account ?: return@map null
if (!account.isOfficial(context)) {
return@map null
}
return@map account.key return@map account.key
}.toTypedArray() }.toTypedArray()
} }

View File

@ -28,6 +28,7 @@ import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.sqliteqb.library.Expression import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.OrderBy import org.mariotaku.sqliteqb.library.OrderBy
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.timestamp import org.mariotaku.twidere.extension.model.timestamp
import org.mariotaku.twidere.extension.queryOne import org.mariotaku.twidere.extension.queryOne
@ -80,6 +81,23 @@ class MarkMessageReadTask(
internal fun performMarkRead(context: Context, microBlog: MicroBlog, account: AccountDetails, internal fun performMarkRead(context: Context, microBlog: MicroBlog, account: AccountDetails,
conversation: ParcelableMessageConversation): Pair<String, Long>? { conversation: ParcelableMessageConversation): Pair<String, Long>? {
val cr = context.contentResolver val cr = context.contentResolver
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
val event = (conversation.conversation_extras as? TwitterOfficialConversationExtras)?.maxReadEvent ?: run {
val message = cr.findRecentMessage(account.key, conversation.id) ?: return null
return@run Pair(message.id, message.timestamp)
}
if (conversation.last_read_timestamp > event.second) {
// Local is newer, ignore network request
return event
}
if (microBlog.markDmRead(conversation.id, event.first).isSuccessful) {
return event
}
}
}
}
val message = cr.findRecentMessage(account.key, conversation.id) ?: return null val message = cr.findRecentMessage(account.key, conversation.id) ?: return null
return Pair(message.id, message.timestamp) return Pair(message.id, message.timestamp)
} }

View File

@ -31,6 +31,7 @@ import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.api.* import org.mariotaku.twidere.extension.model.api.*
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableMedia import org.mariotaku.twidere.model.ParcelableMedia
@ -41,6 +42,7 @@ import org.mariotaku.twidere.model.util.ParcelableMessageUtils
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.task.ExceptionHandlingAbstractTask import org.mariotaku.twidere.task.ExceptionHandlingAbstractTask
import org.mariotaku.twidere.task.twitter.UpdateStatusTask import org.mariotaku.twidere.task.twitter.UpdateStatusTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.addConversation import org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.addConversation
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.addLocalConversations import org.mariotaku.twidere.task.twitter.message.GetMessagesTask.Companion.addLocalConversations
@ -84,7 +86,11 @@ class SendMessageTask(
message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData { message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData {
when (account.type) { when (account.type) {
AccountType.TWITTER -> { AccountType.TWITTER -> {
return sendTwitterMessageEvent(microBlog, account, message) if (account.isOfficial(context)) {
return sendTwitterOfficialDM(microBlog, account, message)
} else {
return sendTwitterMessageEvent(microBlog, account, message)
}
} }
AccountType.FANFOU -> { AccountType.FANFOU -> {
return sendFanfouDM(microBlog, account, message) return sendFanfouDM(microBlog, account, message)
@ -93,6 +99,47 @@ class SendMessageTask(
return sendDefaultDM(microBlog, account, message) return sendDefaultDM(microBlog, account, message)
} }
private fun sendTwitterOfficialDM(microBlog: MicroBlog, account: AccountDetails,
message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData {
var deleteOnSuccess: List<UpdateStatusTask.MediaDeletionItem>? = null
var deleteAlways: List<UpdateStatusTask.MediaDeletionItem>? = null
val sendResponse = try {
val conversationId = message.conversation_id
val tempConversation = message.is_temp_conversation
val newDm = NewDm()
if (!tempConversation && conversationId != null) {
newDm.setConversationId(conversationId)
} else {
newDm.setRecipientIds(message.recipient_ids)
}
newDm.setText(message.text)
if (message.media.isNotNullOrEmpty()) {
val upload = account.newMicroBlogInstance(context, cls = TwitterUpload::class.java)
val uploadResult = UpdateStatusTask.uploadMicroBlogMediaShared(context,
upload, account, message.media, null, null, true, null)
newDm.setMediaId(uploadResult.ids[0])
deleteAlways = uploadResult.deleteAlways
deleteOnSuccess = uploadResult.deleteOnSuccess
}
microBlog.sendDm(newDm)
} catch (e: UpdateStatusTask.UploadException) {
e.deleteAlways?.forEach {
it.delete(context)
}
throw MicroBlogException(e)
} finally {
deleteAlways?.forEach { it.delete(context) }
}
deleteOnSuccess?.forEach { it.delete(context) }
val conversationId = sendResponse.entries?.firstOrNull {
it.message != null
}?.message?.conversationId
val response = microBlog.getDmConversation(conversationId, null).conversationTimeline
return GetMessagesTask.createDatabaseUpdateData(context, account, response, profileImageSize)
}
private fun sendTwitterMessageEvent(microBlog: MicroBlog, account: AccountDetails, private fun sendTwitterMessageEvent(microBlog: MicroBlog, account: AccountDetails,
message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData { message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData {
val recipientId = message.recipient_ids.singleOrNull() ?: throw MicroBlogException("No recipient") val recipientId = message.recipient_ids.singleOrNull() ?: throw MicroBlogException("No recipient")

View File

@ -24,6 +24,7 @@ import android.content.Context
import org.mariotaku.microblog.library.MicroBlog import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.twidere.annotation.AccountType import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.notificationDisabled import org.mariotaku.twidere.extension.model.notificationDisabled
import org.mariotaku.twidere.model.AccountDetails import org.mariotaku.twidere.model.AccountDetails
@ -60,6 +61,24 @@ class SetConversationNotificationDisabledTask(
private fun requestSetNotificationDisabled(microBlog: MicroBlog, account: AccountDetails): private fun requestSetNotificationDisabled(microBlog: MicroBlog, account: AccountDetails):
GetMessagesTask.DatabaseUpdateData { GetMessagesTask.DatabaseUpdateData {
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
val response = if (notificationDisabled) {
microBlog.disableDmConversations(conversationId)
} else {
microBlog.enableDmConversations(conversationId)
}
val conversation = DataStoreUtils.findMessageConversation(context, accountKey,
conversationId) ?: return GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList())
if (response.isSuccessful) {
conversation.notificationDisabled = notificationDisabled
}
return GetMessagesTask.DatabaseUpdateData(listOf(conversation), emptyList())
}
}
}
val conversation = DataStoreUtils.findMessageConversation(context, accountKey, val conversation = DataStoreUtils.findMessageConversation(context, accountKey,
conversationId) ?: return GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList()) conversationId) ?: return GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList())
conversation.notificationDisabled = notificationDisabled conversation.notificationDisabled = notificationDisabled

View File

@ -377,6 +377,18 @@ class AsyncTwitterWrapper(
} }
fun setActivitiesAboutMeUnreadAsync(accountKeys: Array<UserKey>, cursor: Long) { fun setActivitiesAboutMeUnreadAsync(accountKeys: Array<UserKey>, cursor: Long) {
val task = object : ExceptionHandlingAbstractTask<Any?, Unit, MicroBlogException, Any?>(context) {
override val exceptionClass = MicroBlogException::class.java
override fun onExecute(params: Any?) {
for (accountKey in accountKeys) {
val microBlog = MicroBlogAPIFactory.getInstance(context, accountKey) ?: continue
if (!AccountUtils.isOfficial(context, accountKey)) continue
microBlog.setActivitiesAboutMeUnread(cursor)
}
}
}
TaskStarter.execute(task)
} }
fun addUpdatingRelationshipId(accountKey: UserKey, userKey: UserKey) { fun addUpdatingRelationshipId(accountKey: UserKey, userKey: UserKey) {

View File

@ -998,8 +998,23 @@ object DataStoreUtils {
private fun <T> getOfficialSeparatedIds(context: Context, getFromDatabase: (Array<UserKey?>, Boolean) -> T, private fun <T> getOfficialSeparatedIds(context: Context, getFromDatabase: (Array<UserKey?>, Boolean) -> T,
mergeResult: (T, T) -> T, accountKeys: Array<UserKey?>): T { mergeResult: (T, T) -> T, accountKeys: Array<UserKey?>): T {
val officialMaxPositions = getFromDatabase(emptyArray(), true) val officialKeys = Array(accountKeys.size) {
val notOfficialMaxPositions = getFromDatabase(accountKeys, false) val key = accountKeys[it] ?: return@Array null
if (AccountUtils.isOfficial(context, key)) {
return@Array key
}
return@Array null
}
val notOfficialKeys = Array(accountKeys.size) {
val key = accountKeys[it] ?: return@Array null
if (AccountUtils.isOfficial(context, key)) {
return@Array null
}
return@Array key
}
val officialMaxPositions = getFromDatabase(officialKeys, true)
val notOfficialMaxPositions = getFromDatabase(notOfficialKeys, false)
return mergeResult(officialMaxPositions, notOfficialMaxPositions) return mergeResult(officialMaxPositions, notOfficialMaxPositions)
} }
} }

View File

@ -54,6 +54,7 @@ import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.constant.favoriteConfirmationKey import org.mariotaku.twidere.constant.favoriteConfirmationKey
import org.mariotaku.twidere.constant.iWantMyStarsBackKey import org.mariotaku.twidere.constant.iWantMyStarsBackKey
import org.mariotaku.twidere.constant.nameFirstKey import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.fragment.AbsStatusesFragment import org.mariotaku.twidere.fragment.AbsStatusesFragment
import org.mariotaku.twidere.fragment.AddStatusFilterDialogFragment import org.mariotaku.twidere.fragment.AddStatusFilterDialogFragment
import org.mariotaku.twidere.fragment.BaseFragment import org.mariotaku.twidere.fragment.BaseFragment
@ -188,7 +189,11 @@ object MenuUtils {
favorite.setTitle(if (isFavorite) R.string.action_undo_like else R.string.action_like) favorite.setTitle(if (isFavorite) R.string.action_undo_like else R.string.action_like)
} }
} }
menu.setItemAvailability(R.id.translate, false) val translate = menu.findItem(R.id.translate)
if (translate != null) {
val isOfficialKey = details.isOfficial(context)
menu.setItemAvailability(R.id.translate, isOfficialKey)
}
menu.removeGroup(MENU_GROUP_STATUS_EXTENSION) menu.removeGroup(MENU_GROUP_STATUS_EXTENSION)
addIntentToMenuForExtension(context, menu, MENU_GROUP_STATUS_EXTENSION, addIntentToMenuForExtension(context, menu, MENU_GROUP_STATUS_EXTENSION,
INTENT_ACTION_EXTENSION_OPEN_STATUS, EXTRA_STATUS, EXTRA_STATUS_JSON, status) INTENT_ACTION_EXTENSION_OPEN_STATUS, EXTRA_STATUS, EXTRA_STATUS_JSON, status)
@ -279,8 +284,10 @@ object MenuUtils {
DestroyStatusDialogFragment.show(fm, status) DestroyStatusDialogFragment.show(fm, status)
} }
R.id.pin -> { R.id.pin -> {
PinStatusDialogFragment.show(fm, status)
} }
R.id.unpin -> { R.id.unpin -> {
UnpinStatusDialogFragment.show(fm, status)
} }
R.id.add_to_filter -> { R.id.add_to_filter -> {
AddStatusFilterDialogFragment.show(fm, status) AddStatusFilterDialogFragment.show(fm, status)

View File

@ -89,6 +89,13 @@ class ComposeEditText(
} catch (e: AbstractMethodError) { } catch (e: AbstractMethodError) {
// http://crashes.to/s/69acd0ea0de // http://crashes.to/s/69acd0ea0de
return true return true
}catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
// workaround
// https://github.com/TwidereProject/Twidere-Android/issues/1178
setSelection(length() - 1, length() - 1)
setSelection(length(), length())
return true
} }
} }

View File

@ -339,8 +339,26 @@ class DetailStatusViewHolder(
val lang = status.lang val lang = status.lang
translateLabelView.setText(R.string.unknown_language) if (CheckUtils.isValidLocale(lang) && account.isOfficial(context)) {
translateContainer.visibility = View.GONE translateContainer.visibility = View.VISIBLE
if (translation != null) {
val locale = Locale(translation.translatedLang)
translateLabelView.text = context.getString(R.string.label_translated_to_language,
locale.displayLanguage)
translateResultView.visibility = View.VISIBLE
translateChangeLanguageView.visibility = View.VISIBLE
translateResultView.text = translation.text
} else {
val locale = Locale(lang)
translateLabelView.text = context.getString(R.string.label_translate_from_language,
locale.displayLanguage)
translateResultView.visibility = View.GONE
translateChangeLanguageView.visibility = View.GONE
}
} else {
translateLabelView.setText(R.string.unknown_language)
translateContainer.visibility = View.GONE
}
textView.setTextIsSelectable(true) textView.setTextIsSelectable(true)
translateResultView.setTextIsSelectable(true) translateResultView.setTextIsSelectable(true)

View File

@ -29,14 +29,14 @@
<!-- [verb] Edit image/settings etc. --> <!-- [verb] Edit image/settings etc. -->
<string name="action_edit">Edita</string> <string name="action_edit">Edita</string>
<string name="action_favorite">Aggiungi ai preferiti</string> <string name="action_favorite">Aggiungi ai preferiti</string>
<string name="action_finish">Finish</string> <string name="action_finish">Fine</string>
<string name="action_follow">Segui</string> <string name="action_follow">Segui</string>
<!-- [verb] Perform import action --> <!-- [verb] Perform import action -->
<string name="action_import_from">Importa da&#8230;</string> <string name="action_import_from">Importa da&#8230;</string>
<!-- Used for decide something later, like permission request --> <!-- Used for decide something later, like permission request -->
<!-- [verb] e.g. An action label on a tweet to like this tweet. Formerly Twitter favorite. --> <!-- [verb] e.g. An action label on a tweet to like this tweet. Formerly Twitter favorite. -->
<string name="action_like">Like</string> <string name="action_like">Mi piace</string>
<string name="action_mute">Muto</string> <string name="action_mute">Silenzia</string>
<string name="action_name_saved_at_time"><xliff:g id="action">%1$s</xliff:g>, salvato alle <xliff:g id="time">%2$s</xliff:g></string> <string name="action_name_saved_at_time"><xliff:g id="action">%1$s</xliff:g>, salvato alle <xliff:g id="time">%2$s</xliff:g></string>
<string name="action_open_in_browser">Apri nel browser</string> <string name="action_open_in_browser">Apri nel browser</string>
<string name="action_pick_color">Scegli colore</string> <string name="action_pick_color">Scegli colore</string>
@ -47,7 +47,7 @@
<!-- [verb] Restore purchase --> <!-- [verb] Restore purchase -->
<string name="action_retry">Riprova</string> <string name="action_retry">Riprova</string>
<!-- [verb] Action for performing retweet --> <!-- [verb] Action for performing retweet -->
<string name="action_retweet">Retweet</string> <string name="action_retweet">Ritwitta</string>
<!-- [verb] Save settings/files etc. --> <!-- [verb] Save settings/files etc. -->
<string name="action_save">Salva</string> <string name="action_save">Salva</string>
<string name="action_search">Cerca</string> <string name="action_search">Cerca</string>
@ -63,17 +63,17 @@
<!-- [verb] Disconnect from network storage --> <!-- [verb] Disconnect from network storage -->
<string name="action_sync_settings">Impostazioni</string> <string name="action_sync_settings">Impostazioni</string>
<string name="action_take_photo">Scatta una foto</string> <string name="action_take_photo">Scatta una foto</string>
<string name="action_toggle">Toggle</string> <string name="action_toggle">Apri</string>
<string name="action_translate">Traduce</string> <string name="action_translate">Traduci</string>
<string name="action_twitter_mute_user">Filtra utente</string> <string name="action_twitter_mute_user">Filtra utente</string>
<string name="action_twitter_muted_users">Utenti filtrati</string> <string name="action_twitter_muted_users">Utenti filtrati</string>
<!-- [verb] Action for unblocking user --> <!-- [verb] Action for unblocking user -->
<string name="action_unblock">Sblocca</string> <string name="action_unblock">Sblocca</string>
<string name="action_undo_like">Annulla like</string> <string name="action_undo_like">Annulla like</string>
<string name="action_unfavorite">Togli dai preferiti</string> <string name="action_unfavorite">Rimuovi dai preferiti</string>
<string name="action_unfollow">Smetti di seguire</string> <string name="action_unfollow">Smetti di seguire</string>
<string name="action_unmute">Muto off</string> <string name="action_unmute">Riattiva</string>
<string name="action_unsubscribe">Cancellati</string> <string name="action_unsubscribe">Disiscriviti</string>
<string name="action_view_map">Mappa</string> <string name="action_view_map">Mappa</string>
<string name="activated_accounts">Account attivati</string> <string name="activated_accounts">Account attivati</string>
<string name="activities_about_me">Mie Attività</string> <string name="activities_about_me">Mie Attività</string>
@ -82,7 +82,7 @@
<string name="add_image">Aggiungi immagine</string> <string name="add_image">Aggiungi immagine</string>
<string name="and_N_more">e altri <xliff:g id="count">%d</xliff:g></string> <string name="and_N_more">e altri <xliff:g id="count">%d</xliff:g></string>
<string name="api_url_format">Formato API URL</string> <string name="api_url_format">Formato API URL</string>
<string name="app_description">La tua Twitter app</string> <string name="app_description">La tua app per Twitter</string>
<!-- App name, normally you don't need to translate this. --> <!-- App name, normally you don't need to translate this. -->
<string name="app_name">Twidere</string> <string name="app_name">Twidere</string>
<string name="app_restart_confirm">Twidere si riavvierà per rendere effettive le nuove le impostazioni.</string> <string name="app_restart_confirm">Twidere si riavvierà per rendere effettive le nuove le impostazioni.</string>
@ -92,7 +92,7 @@
<string name="auth_type_twip_o">Modalità twip O</string> <string name="auth_type_twip_o">Modalità twip O</string>
<string name="auth_type_xauth">xAuth</string> <string name="auth_type_xauth">xAuth</string>
<string name="auto_refresh">Auto refresh</string> <string name="auto_refresh">Auto refresh</string>
<string name="background">Background</string> <string name="background">Sfondo</string>
<string name="bandwidth_saving_mode">Modalità risparmio dati</string> <string name="bandwidth_saving_mode">Modalità risparmio dati</string>
<string name="bandwidth_saving_mode_summary">Disabilita l\'anteprima dei media su connessione a consumo</string> <string name="bandwidth_saving_mode_summary">Disabilita l\'anteprima dei media su connessione a consumo</string>
<string name="belongs_to">Appartiene a</string> <string name="belongs_to">Appartiene a</string>
@ -116,11 +116,11 @@
<string name="comment_hint">Commento&#8230;</string> <string name="comment_hint">Commento&#8230;</string>
<string name="compact_cards">Schede compatte</string> <string name="compact_cards">Schede compatte</string>
<string name="compact_cards_summary">Visualizza più schede sullo schermo</string> <string name="compact_cards_summary">Visualizza più schede sullo schermo</string>
<string name="compose_now">Compose Now</string> <string name="compose_now">Componimento veloce</string>
<string name="compose_now_action">Azione Compose Now</string> <string name="compose_now_action">Azione Componimento veloce</string>
<string name="compose_now_summary">Sostituisce la scorciatoia a Google Now con la Compose screen</string> <string name="compose_now_summary">Sostituisce la scorciatoia a Google Now con la schemata di composizione</string>
<string name="conflicts_with_name">Confitti con <xliff:g id="name">%s</xliff:g></string> <string name="conflicts_with_name">Confitti con <xliff:g id="name">%s</xliff:g></string>
<string name="connection_timeout">Timeout connessione</string> <string name="connection_timeout">Timeout per la connessione</string>
<string name="consumer_key">Consumer Key</string> <string name="consumer_key">Consumer Key</string>
<string name="consumer_secret">Consumer secret</string> <string name="consumer_secret">Consumer secret</string>
<string name="content">Contenuto</string> <string name="content">Contenuto</string>
@ -141,19 +141,19 @@
<string name="default_api_settings">Impostazioni di default API</string> <string name="default_api_settings">Impostazioni di default API</string>
<string name="default_api_settings_summary">Queste impostazioni veranno applicate al prossimo login</string> <string name="default_api_settings_summary">Queste impostazioni veranno applicate al prossimo login</string>
<string name="default_ringtone">Suoneria predefinita</string> <string name="default_ringtone">Suoneria predefinita</string>
<string name="delete_conversation">Cancella conversazione</string> <string name="delete_conversation">Elimina conversazione</string>
<string name="delete_conversation_confirm_message">Cancellare tutti i messaggi di questa conversazione?</string> <string name="delete_conversation_confirm_message">Eliminare tutti i messaggi di questa conversazione?</string>
<string name="delete_drafts_confirm">Eliminare le bozze selezionate?</string> <string name="delete_drafts_confirm">Eliminare le bozze selezionate?</string>
<string name="delete_message_confirm_message">Cancellare questo messaggio?</string> <string name="delete_message_confirm_message">Cancellare questo messaggio?</string>
<string name="delete_user">Elimina utente <xliff:g id="name">%s</xliff:g></string> <string name="delete_user">Elimina utente <xliff:g id="name">%s</xliff:g></string>
<string name="delete_user_confirm_message">Eliminare <xliff:g id="name">%s</xliff:g>? Non è reversibile.</string> <string name="delete_user_confirm_message">Eliminare <xliff:g id="name">%s</xliff:g>? Non è reversibile.</string>
<string name="delete_user_from_list_confirm">Rimuovere <xliff:g id="user">%1$s</xliff:g> dalla lista "<xliff:g id="list">%2$s</xliff:g>\"?</string> <string name="delete_user_from_list_confirm">Rimuovere <xliff:g id="user">%1$s</xliff:g> dalla lista "<xliff:g id="list">%2$s</xliff:g>\"?</string>
<string name="delete_user_list">Elimina lista <xliff:g id="name">%s</xliff:g></string> <string name="delete_user_list">Elimina lista <xliff:g id="name">%s</xliff:g></string>
<string name="delete_user_list_confirm_message">Eliminare lisra <xliff:g id="name">%s</xliff:g>? Non potrà essere annullato.</string> <string name="delete_user_list_confirm_message">Eliminare lista <xliff:g id="name">%s</xliff:g>? Non potrà essere annullato.</string>
<string name="delete_users">Elimina utenti</string> <string name="delete_users">Elimina utenti</string>
<string name="deleted_list">Lista cancellata \"<xliff:g id="list">%s</xliff:g>\".</string> <string name="deleted_list">La lista \"<xliff:g id="list">%s</xliff:g>\" è stata eliminata.</string>
<string name="deleted_user_from_list">Rimosso <xliff:g id="user">%1$s</xliff:g> dalla lista "<xliff:g id="list">%2$s</xliff:g>\".</string> <string name="deleted_user_from_list">Rimosso <xliff:g id="user">%1$s</xliff:g> dalla lista "<xliff:g id="list">%2$s</xliff:g>\".</string>
<string name="denied_users_follow_request">Richiesta <xliff:g id="user">%s</xliff:g> di follow negata.</string> <string name="denied_users_follow_request">Richiesta di follow di <xliff:g id="user">%s</xliff:g> negata.</string>
<string name="deny">Rifiuta</string> <string name="deny">Rifiuta</string>
<string name="destroy_saved_search">Elimina ricerca salvata \"<xliff:g id="name">%s</xliff:g>\"</string> <string name="destroy_saved_search">Elimina ricerca salvata \"<xliff:g id="name">%s</xliff:g>\"</string>
<string name="destroy_saved_search_confirm_message">Eliminare la ricerca \"<xliff:g id="name">%s</xliff:g>\"? È possibile salvarla nuovamente in seguito.</string> <string name="destroy_saved_search_confirm_message">Eliminare la ricerca \"<xliff:g id="name">%s</xliff:g>\"? È possibile salvarla nuovamente in seguito.</string>
@ -200,19 +200,19 @@
<!-- Enhanced (paid) features title --> <!-- Enhanced (paid) features title -->
<string name="fast_image_loading">Caricamento veloce immagini</string> <string name="fast_image_loading">Caricamento veloce immagini</string>
<string name="fast_image_loading_summary">Seleziona per far caricare più velocemente le immagini, disattivalo se alcune immagini non sono mostrate correttamente.</string> <string name="fast_image_loading_summary">Seleziona per far caricare più velocemente le immagini, disattivalo se alcune immagini non sono mostrate correttamente.</string>
<string name="filter_type_keywords">Keywords</string> <string name="filter_type_keywords">Parole chiave</string>
<string name="filter_type_links">Collegamenti</string> <string name="filter_type_links">Collegamenti</string>
<string name="filter_type_sources">Fonti</string> <string name="filter_type_sources">Fonti</string>
<string name="filter_type_users">Utenti</string> <string name="filter_type_users">Utenti</string>
<string name="follow_request_sent">Richiesta di follow inviata</string> <string name="follow_request_sent">Richiesta di follow inviata</string>
<string name="followed_user">Seguito <xliff:g id="user">%s</xliff:g>.</string> <string name="followed_user">Seguito <xliff:g id="user">%s</xliff:g>.</string>
<string name="following_only">I tuoi following</string> <string name="following_only">Persone che segui</string>
<string name="following_only_summary">Mostra notifiche solo dagli utenti che segui.</string> <string name="following_only_summary">Mostra notifiche solo dagli utenti che segui.</string>
<string name="following_you">Chi ti segue</string> <string name="following_you">Chi ti segue</string>
<string name="follows">Segui</string> <string name="follows">Segui</string>
<string name="font">Carattere</string> <string name="font">Carattere</string>
<string name="font_family">Font</string> <string name="font_family">Font</string>
<string name="from_camera">Da Camera</string> <string name="from_camera">Da fotocamera</string>
<string name="from_gallery">Da galleria</string> <string name="from_gallery">Da galleria</string>
<string name="from_name">Da <xliff:g id="name">%1$s</xliff:g></string> <string name="from_name">Da <xliff:g id="name">%1$s</xliff:g></string>
<string name="from_name_and_N_others">Da <xliff:g id="name">%1$s</xliff:g> e <xliff:g id="name">%2$d</xliff:g> altri</string> <string name="from_name_and_N_others">Da <xliff:g id="name">%1$s</xliff:g> e <xliff:g id="name">%2$d</xliff:g> altri</string>
@ -227,8 +227,8 @@
<string name="hashtag">Hashtag</string> <string name="hashtag">Hashtag</string>
<string name="hidden_settings">Impostazioni nascoste</string> <string name="hidden_settings">Impostazioni nascoste</string>
<string name="hidden_settings_warning_message">MAI cambiare queste impostazioni se non sai esattamente cosa fanno, o potrebbero:\n * Uccidere il tuo gatto \n * Lanciare testate nucleari in Corea del Nord \n * fArtI tWitTtarE CM 1 PaxxErEllo\n * Distruggere l\'universo</string> <string name="hidden_settings_warning_message">MAI cambiare queste impostazioni se non sai esattamente cosa fanno, o potrebbero:\n * Uccidere il tuo gatto \n * Lanciare testate nucleari in Corea del Nord \n * fArtI tWitTtarE CM 1 PaxxErEllo\n * Distruggere l\'universo</string>
<string name="hidden_settings_warning_title">ATTENZIONE: queste opzioni possono far male!</string> <string name="hidden_settings_warning_title">ATTENZIONE: queste opzioni creare danni!</string>
<string name="hide_card_actions">Nascondi azioni di carte</string> <string name="hide_card_actions">Nascondi le azioni per il tweet</string>
<string name="hide_quotes">Nascondi citazioni</string> <string name="hide_quotes">Nascondi citazioni</string>
<string name="hide_replies">Nascondi risposte</string> <string name="hide_replies">Nascondi risposte</string>
<string name="hide_retweets">Nascondi retweets</string> <string name="hide_retweets">Nascondi retweets</string>
@ -241,18 +241,18 @@
<string name="host_mapping_address">Indirizzo (può essere un altro indirizzo host)</string> <string name="host_mapping_address">Indirizzo (può essere un altro indirizzo host)</string>
<string name="host_mapping_host">Host</string> <string name="host_mapping_host">Host</string>
<string name="i_want_my_stars_back">Rivoglio le mie stelline!</string> <string name="i_want_my_stars_back">Rivoglio le mie stelline!</string>
<string name="i_want_my_stars_back_summary">Usa i preferiti (★) piuttosto che i like (♥︎)</string> <string name="i_want_my_stars_back_summary">Usa i preferiti (★) al posto dei mi piace (♥︎)</string>
<string name="icon">Icona</string> <string name="icon">Icona</string>
<string name="icon_restored_message">Icona ripristinata!</string> <string name="icon_restored_message">Icona ripristinata!</string>
<string name="import_export_settings">Importa/Esporta le impostazioni</string> <string name="import_export_settings">Importa/Esporta le impostazioni</string>
<string name="import_settings">Importa impostazioni</string> <string name="import_settings">Importa impostazioni</string>
<string name="import_settings_type_dialog_title">Importa impostazioni&#8230;</string> <string name="import_settings_type_dialog_title">Importa impostazioni&#8230;</string>
<string name="in_reply_to_name">Risposta a <xliff:g id="user_name">%s</xliff:g></string> <string name="in_reply_to_name">Rispondi a <xliff:g id="user_name">%s</xliff:g></string>
<string name="inbox">Inbox</string> <string name="inbox">Inbox</string>
<string name="incoming_friendships">Richieste di seguirti in attesa</string> <string name="incoming_friendships">Richieste di follow in attesa</string>
<string name="input_text">Immissione testo</string> <string name="input_text">Immetti testo</string>
<string name="interactions">Interazioni</string> <string name="interactions">Interazioni</string>
<string name="invalid_consumer_key">Chaive da consumatore invalida</string> <string name="invalid_consumer_key">Chiave da consumatore invalida</string>
<string name="invalid_consumer_secret">Segreto consumatore invalido</string> <string name="invalid_consumer_secret">Segreto consumatore invalido</string>
<string name="invalid_list_name">Deve iniziare con una lettera e può contenere solo lettere, numeri, \"-\" o \"_\".</string> <string name="invalid_list_name">Deve iniziare con una lettera e può contenere solo lettere, numeri, \"-\" o \"_\".</string>
<string name="invalid_tab">Tab non valida</string> <string name="invalid_tab">Tab non valida</string>
@ -261,9 +261,9 @@
<string name="keyboard_shortcut_back">Indietro</string> <string name="keyboard_shortcut_back">Indietro</string>
<string name="keyboard_shortcut_hint">Premi i tasti</string> <string name="keyboard_shortcut_hint">Premi i tasti</string>
<string name="keyboard_shortcuts">Scorciatoie da tastiera</string> <string name="keyboard_shortcuts">Scorciatoie da tastiera</string>
<string name="keyword_filter_name">Keyword: <xliff:g id="name">%s</xliff:g></string> <string name="keyword_filter_name">Parola chiave: <xliff:g id="name">%s</xliff:g></string>
<string name="label_auth_type">Tipo di autenticazione</string> <string name="label_auth_type">Tipo di autenticazione</string>
<string name="label_background_operation_service">Background operation service</string> <string name="label_background_operation_service">Servizio di operazione in background</string>
<!-- General Audiences. All ages admitted. http://www.mpaa.org/film-ratings/ --> <!-- General Audiences. All ages admitted. http://www.mpaa.org/film-ratings/ -->
<!-- Parental Guidance Suggested. Some material may not be suitable for children. http://www.mpaa.org/film-ratings/ --> <!-- Parental Guidance Suggested. Some material may not be suitable for children. http://www.mpaa.org/film-ratings/ -->
<!-- Parents Strongly Cautioned. Some material may be inappropriate for children under 13. http://www.mpaa.org/film-ratings/ --> <!-- Parents Strongly Cautioned. Some material may be inappropriate for children under 13. http://www.mpaa.org/film-ratings/ -->
@ -318,11 +318,11 @@
<string name="members">Membri</string> <string name="members">Membri</string>
<string name="mention_this_user">Menziona questo utente</string> <string name="mention_this_user">Menziona questo utente</string>
<string name="mention_user">Menziona <xliff:g id="user">%s</xliff:g></string> <string name="mention_user">Menziona <xliff:g id="user">%s</xliff:g></string>
<string name="mention_user_name">Mention <xliff:g id="name">%1$s</xliff:g></string> <string name="mention_user_name">Menziona <xliff:g id="name">%1$s</xliff:g></string>
<string name="mentions_only">Solo menzioni</string> <string name="mentions_only">Solo menzioni</string>
<string name="message_api_data_corrupted">Dati delle API corrotti.</string> <string name="message_api_data_corrupted">Dati delle API corrotti.</string>
<string name="message_api_url_format_help">[DOMAIN]: Twitter API domain.\nPer Esempio: https://[DOMAIN].twitter.com/ sarà sostituito da https://api.twitter.com/.</string> <string name="message_api_url_format_help">[DOMAIN]: Dominio dell\' API di Twitter.\nPer Esempio: https://[DOMAIN].twitter.com/ sarà sostituito da https://api.twitter.com/.</string>
<string name="message_blocked_user">Bloccato <xliff:g id="user">%s</xliff:g>.</string> <string name="message_blocked_user"><xliff:g id="user">%s</xliff:g> bloccato.</string>
<string name="message_direct_message_deleted">Messaggio diretto cancellato.</string> <string name="message_direct_message_deleted">Messaggio diretto cancellato.</string>
<string name="message_direct_message_sent">Messaggio diretto inviato.</string> <string name="message_direct_message_sent">Messaggio diretto inviato.</string>
<string name="message_please_wait">Attendere prego.</string> <string name="message_please_wait">Attendere prego.</string>
@ -336,30 +336,30 @@
<!-- Toast message for enhanced (paid) features not purchased while trying to restore purchase --> <!-- Toast message for enhanced (paid) features not purchased while trying to restore purchase -->
<string name="message_toast_link_copied_to_clipboard">Link copiato negli appunti</string> <string name="message_toast_link_copied_to_clipboard">Link copiato negli appunti</string>
<string name="message_toast_livewp_daydream_enabled_message">Ecco un regalino per te, trovalo tra le impostazioni di sistema :)</string> <string name="message_toast_livewp_daydream_enabled_message">Ecco un regalino per te, trovalo tra le impostazioni di sistema :)</string>
<string name="message_toast_login_verification_failed">Verifica di login fallita.</string> <string name="message_toast_login_verification_failed">Verifica del login fallita.</string>
<!-- Toast message for network errors --> <!-- Toast message for network errors -->
<string name="message_toast_no_account">Nessun account</string> <string name="message_toast_no_account">Nessun account</string>
<string name="message_toast_no_account_selected">Nessun account selezionato.</string> <string name="message_toast_no_account_selected">Nessun account selezionato.</string>
<string name="message_toast_press_again_to_close">Premi di nuovo per chiudere</string> <string name="message_toast_press_again_to_close">Premi di nuovo per chiudere</string>
<string name="message_toast_profile_banner_image_updated">Immagine dell\'intestazione profilo aggiornata.</string> <string name="message_toast_profile_banner_image_updated">Immagine dell\'intestazione profilo aggiornata.</string>
<string name="message_toast_save_media_no_storage_permission">Permesso di memoria richiesto per salvare questo media.</string> <string name="message_toast_save_media_no_storage_permission">Permesso di memoria richiesto per salvare questo media.</string>
<string name="message_toast_saved_to_gallery">Salvato in Galleria.</string> <string name="message_toast_saved_to_gallery">Salvato nella Galleria.</string>
<string name="message_toast_select_file_no_storage_permission">Permessi di memoria richiesti per selezionare il file.</string> <string name="message_toast_select_file_no_storage_permission">Permessi di memoria richiesti per selezionare il file.</string>
<string name="message_toast_share_media_no_storage_permission">Alcune applicazioni richiedono permessi per condividere alcuni media.</string> <string name="message_toast_share_media_no_storage_permission">Alcune applicazioni richiedono permessi per condividere alcuni media.</string>
<string name="message_toast_status_saved_to_draft">Tweet salvato in bozze.</string> <string name="message_toast_status_saved_to_draft">Tweet salvato nelle bozze.</string>
<string name="message_toast_status_updated">Tweet inviato.</string> <string name="message_toast_status_updated">Tweet inviato.</string>
<string name="message_toast_wrong_api_key">URL delle API non corretto, o consumer key/secret errata. Per favore, controllale.</string> <string name="message_toast_wrong_api_key">URL delle API non corretto, o consumer key/secret errata. Per favore, controllale.</string>
<string name="multi_select">Selezione multipla</string> <string name="multi_select">Selezione multipla</string>
<string name="mute_user">Muto <xliff:g id="name">%s</xliff:g></string> <string name="mute_user">Silenzia <xliff:g id="name">%s</xliff:g></string>
<string name="mute_user_confirm_message">Silenziare <xliff:g id="name">%s</xliff:g>? Non vedrai più i tweets di questo utente ma continuerai a seguirlo.</string> <string name="mute_user_confirm_message">Silenziare <xliff:g id="name">%s</xliff:g>? Non vedrai più i tweets di questo utente ma continuerai a seguirlo.</string>
<string name="muted_user">Muto on <xliff:g id="name">%s</xliff:g></string> <string name="muted_user"><xliff:g id="name">%s</xliff:g> silenziato</string>
<string name="name_and_another_retweeted"><xliff:g id="user_name">%1$s</xliff:g> e un altro hanno ReTweettato</string> <string name="name_and_another_retweeted"><xliff:g id="user_name">%1$s</xliff:g> e un altro hanno Ritwittato</string>
<string name="name_and_count_retweeted">Retwittato da <xliff:g id="user_name">%1$s</xliff:g> e da altri <xliff:g id="retweet_count">%2$d</xliff:g></string> <string name="name_and_count_retweeted">Ritwittato da <xliff:g id="user_name">%1$s</xliff:g> e da altri <xliff:g id="retweet_count">%2$d</xliff:g></string>
<string name="name_first">Mostra il nome prima</string> <string name="name_first">Mostra il nome prima</string>
<string name="name_first_summary_off">Display \@nomeschermo primo</string> <string name="name_first_summary_off">Mostra \@nomeschermo per primo</string>
<string name="name_first_summary_on">Visualizza prima il nome</string> <string name="name_first_summary_on">Visualizza prima il nome</string>
<string name="name_not_set"><xliff:g id="name">%s</xliff:g> non settato</string> <string name="name_not_set"><xliff:g id="name">%s</xliff:g> non settato</string>
<string name="name_retweeted">ReTweettato da <xliff:g id="user_name">%s</xliff:g></string> <string name="name_retweeted">Ritwittato da <xliff:g id="user_name">%s</xliff:g></string>
<string name="name_with_nickname"><xliff:g id="name">%1$s</xliff:g> (<xliff:g id="nickname">%2$s</xliff:g>)</string> <string name="name_with_nickname"><xliff:g id="name">%1$s</xliff:g> (<xliff:g id="nickname">%2$s</xliff:g>)</string>
<string name="navigation">Navigazione</string> <string name="navigation">Navigazione</string>
<string name="network">Rete</string> <string name="network">Rete</string>
@ -420,7 +420,7 @@
<string name="permission_description_write">Scrittura sul database, aggiornamento degli stati</string> <string name="permission_description_write">Scrittura sul database, aggiornamento degli stati</string>
<string name="permission_label_shorten_status">Riduci il tweet</string> <string name="permission_label_shorten_status">Riduci il tweet</string>
<string name="permission_label_sync_timeline">Sincronizza timeline</string> <string name="permission_label_sync_timeline">Sincronizza timeline</string>
<string name="permission_label_upload_media">Upload media</string> <string name="permission_label_upload_media">Carica media</string>
<string name="permissions_request">Richiesta di permessi</string> <string name="permissions_request">Richiesta di permessi</string>
<string name="permissions_request_message">L\'app richiede i seguenti permessi</string> <string name="permissions_request_message">L\'app richiede i seguenti permessi</string>
<string name="phishing_link_warning">Avviso di rischio phishing</string> <string name="phishing_link_warning">Avviso di rischio phishing</string>
@ -429,11 +429,11 @@
<string name="phishing_link_warning_summary">Apre un avvertimento quando stai per aprire un link potenzialmente pericoloso in un DM.</string> <string name="phishing_link_warning_summary">Apre un avvertimento quando stai per aprire un link potenzialmente pericoloso in un DM.</string>
<string name="photo">Foto</string> <string name="photo">Foto</string>
<string name="pick_file">Seleziona file</string> <string name="pick_file">Seleziona file</string>
<string name="play">Play</string> <string name="play">Riproduci</string>
<string name="poll_summary_format"><xliff:g id="poll_count">%1$s</xliff:g> · <xliff:g id="poll_time_left">%2$s</xliff:g></string> <string name="poll_summary_format"><xliff:g id="poll_count">%1$s</xliff:g> · <xliff:g id="poll_time_left">%2$s</xliff:g></string>
<string name="preference_summary_auto_refresh_power_saving">Interrompi auto-aggiornamento quando la batteria è scarica</string> <string name="preference_summary_auto_refresh_power_saving">Interrompi auto-aggiornamento quando la batteria è scarica</string>
<string name="preference_summary_database_item_limit">Limite massimo degli elementi memorizzati nel database per ogni account, impostare su un valore inferiore per risparmiare spazio e aumentare la velocità di caricamento.</string> <string name="preference_summary_database_item_limit">Limite massimo degli elementi memorizzati nel database per ogni account, impostare su un valore inferiore per risparmiare spazio e aumentare la velocità di caricamento.</string>
<string name="preference_title_accounts">Accounts</string> <string name="preference_title_accounts">Profili</string>
<string name="preference_title_advanced">Avanzate</string> <string name="preference_title_advanced">Avanzate</string>
<string name="preference_title_database_item_limit">Limite dimensione database</string> <string name="preference_title_database_item_limit">Limite dimensione database</string>
<string name="preference_title_landscape">Landscape</string> <string name="preference_title_landscape">Landscape</string>
@ -441,7 +441,7 @@
<string name="preference_title_storage">Archivio</string> <string name="preference_title_storage">Archivio</string>
<string name="preference_title_streaming_enabled">Attiva streaming</string> <string name="preference_title_streaming_enabled">Attiva streaming</string>
<string name="preference_title_text_size">Dimensione del testo</string> <string name="preference_title_text_size">Dimensione del testo</string>
<string name="preference_title_translate">Traduce</string> <string name="preference_title_translate">Traduci</string>
<string name="preference_title_trends_location">Località per i Trends</string> <string name="preference_title_trends_location">Località per i Trends</string>
<string name="preload_wifi_only">Precarica solo se in Wi-Fi</string> <string name="preload_wifi_only">Precarica solo se in Wi-Fi</string>
<string name="preview">Anteprima</string> <string name="preview">Anteprima</string>
@ -465,7 +465,7 @@
<string name="profile_text_color">Colore testo</string> <string name="profile_text_color">Colore testo</string>
<string name="profile_updated">Profilo aggiornato.</string> <string name="profile_updated">Profilo aggiornato.</string>
<string name="progress">Avanzamento</string> <string name="progress">Avanzamento</string>
<string name="project_account">Project account</string> <string name="project_account">Account del progetto</string>
<string name="projects_we_took_part">Progetti di cui siamo parte</string> <string name="projects_we_took_part">Progetti di cui siamo parte</string>
<!-- Normally you don't need to translate this --> <!-- Normally you don't need to translate this -->
<string name="provider_default">Twidere</string> <string name="provider_default">Twidere</string>
@ -515,9 +515,9 @@
<string name="reset_keyboard_shortcuts_confirm">Resettare le scorciatoie da tastiera ai valori di default?</string> <string name="reset_keyboard_shortcuts_confirm">Resettare le scorciatoie da tastiera ai valori di default?</string>
<string name="reset_to_default">Ripristina come da default</string> <string name="reset_to_default">Ripristina come da default</string>
<string name="retry_on_network_issue">Riprova se si verifica un errore di rete</string> <string name="retry_on_network_issue">Riprova se si verifica un errore di rete</string>
<string name="retweeted_by_count">ReTweettato da <xliff:g id="retweet_count">%d</xliff:g> utenti</string> <string name="retweeted_by_count">Ritwittato da <xliff:g id="retweet_count">%d</xliff:g> utenti</string>
<string name="retweeted_by_name">ReTweettato da <xliff:g id="user_name">%s</xliff:g></string> <string name="retweeted_by_name">Ritwittato da <xliff:g id="user_name">%s</xliff:g></string>
<string name="retweeted_by_name_with_count">ReTweettato da <xliff:g id="user_name">%1$s</xliff:g> e altri <xliff:g id="retweet_count">%2$d</xliff:g></string> <string name="retweeted_by_name_with_count">Ritwittato da <xliff:g id="user_name">%1$s</xliff:g> e altri <xliff:g id="retweet_count">%2$d</xliff:g></string>
<string name="retweets_of_me">I miei retweets</string> <string name="retweets_of_me">I miei retweets</string>
<string name="revoke_permissions">Revoca permessi</string> <string name="revoke_permissions">Revoca permessi</string>
<string name="round">Tonda</string> <string name="round">Tonda</string>
@ -526,9 +526,9 @@
<string name="save_to_gallery">Salva nella galleria</string> <string name="save_to_gallery">Salva nella galleria</string>
<string name="saved_searches">Ricerche salvate</string> <string name="saved_searches">Ricerche salvate</string>
<string name="saved_searches_already_saved_hint">Forse hai già salvato questa ricerca</string> <string name="saved_searches_already_saved_hint">Forse hai già salvato questa ricerca</string>
<string name="scheduled_statuses">Tweet schedulati</string> <string name="scheduled_statuses">Tweet programmati</string>
<string name="scrapyard">Discarica</string> <string name="scrapyard">Discarica</string>
<string name="search_hint">Cerca tweets o utenti</string> <string name="search_hint">Cerca tweet o utenti</string>
<string name="search_statuses">Ricerca tweet</string> <string name="search_statuses">Ricerca tweet</string>
<string name="search_type_statuses">Tweet</string> <string name="search_type_statuses">Tweet</string>
<string name="search_type_users">Utenti</string> <string name="search_type_users">Utenti</string>
@ -619,7 +619,7 @@
<string name="timeline_streaming_running">Timeline di streaming in esecuzione</string> <string name="timeline_streaming_running">Timeline di streaming in esecuzione</string>
<string name="timeline_sync_service">Servizio sincronizzazione timeline</string> <string name="timeline_sync_service">Servizio sincronizzazione timeline</string>
<string name="title_about">Informazioni</string> <string name="title_about">Informazioni</string>
<string name="title_accounts">Accounts</string> <string name="title_accounts">Profili</string>
<string name="title_add_or_remove_from_list">Aggiungi o rimuovi dalla lista</string> <string name="title_add_or_remove_from_list">Aggiungi o rimuovi dalla lista</string>
<string name="title_block_user">Blocca <xliff:g id="name">%s</xliff:g></string> <string name="title_block_user">Blocca <xliff:g id="name">%s</xliff:g></string>
<string name="title_blocked_users">Utenti bloccati</string> <string name="title_blocked_users">Utenti bloccati</string>
@ -635,7 +635,7 @@
<string name="title_favorites">Preferiti</string> <string name="title_favorites">Preferiti</string>
<string name="title_filters">Filtri</string> <string name="title_filters">Filtri</string>
<string name="title_filters_subscription_url">URL</string> <string name="title_filters_subscription_url">URL</string>
<string name="title_followers">Followers</string> <string name="title_followers">Follower</string>
<string name="title_following">Stai seguendo</string> <string name="title_following">Stai seguendo</string>
<string name="title_home">Home</string> <string name="title_home">Home</string>
<!-- [noun] Like, Formerly Twitter's favorite, in the plural --> <!-- [noun] Like, Formerly Twitter's favorite, in the plural -->
@ -654,17 +654,17 @@
<string name="title_user">Utenti</string> <string name="title_user">Utenti</string>
<string name="title_user_colors">Colori personalizzati</string> <string name="title_user_colors">Colori personalizzati</string>
<string name="title_user_list_memberships">Appartiene a</string> <string name="title_user_list_memberships">Appartiene a</string>
<string name="title_users_favorited_this">Users che hanno inserito tra i preferiti</string> <string name="title_users_favorited_this">Utenti che hanno inserito questo elemento tra i preferiti</string>
<string name="title_users_liked_this">Utenti a cui piace</string> <string name="title_users_liked_this">Utenti a cui piace</string>
<string name="title_users_retweeted_this">Utenti che hanno retwittato questo</string> <string name="title_users_retweeted_this">Utenti che hanno retwittato questo elemento</string>
<string name="translation_destination">Lingua</string> <string name="translation_destination">Lingua</string>
<string name="translators">Traduttori</string> <string name="translators">Traduttori</string>
<string name="trends">Trends</string> <string name="trends">Tendenze</string>
<string name="trends_location">Località per i Trends</string> <string name="trends_location">Località per le tendenze</string>
<string name="trends_location_summary">Scegli la località per i trends.</string> <string name="trends_location_summary">Scegli la località per le tendenze.</string>
<!-- 'Tweet' here is a verb --> <!-- 'Tweet' here is a verb -->
<string name="tweet_from_name">Tweet da <xliff:g id="text">%1$s</xliff:g></string> <string name="tweet_from_name">Tweet da <xliff:g id="text">%1$s</xliff:g></string>
<string name="tweet_hashtag">Tweet #<xliff:g id="text">%1$s</xliff:g></string> <string name="tweet_hashtag">Twitta #<xliff:g id="text">%1$s</xliff:g></string>
<string name="twidere_test">Twidere test</string> <string name="twidere_test">Twidere test</string>
<string name="type_name_to_search">Digita un nome da cercare</string> <string name="type_name_to_search">Digita un nome da cercare</string>
<string name="type_to_compose">Digita per comporre</string> <string name="type_to_compose">Digita per comporre</string>
@ -676,12 +676,12 @@
<string name="uninstall">Disinstalla</string> <string name="uninstall">Disinstalla</string>
<string name="unknown_language">Lingua sconosciuta</string> <string name="unknown_language">Lingua sconosciuta</string>
<string name="unknown_location">Posizione sconosciuta</string> <string name="unknown_location">Posizione sconosciuta</string>
<string name="unmute_user">Muto off <xliff:g id="name">%s</xliff:g></string> <string name="unmute_user">Riattiva <xliff:g id="name">%s</xliff:g></string>
<string name="unmuted_user">Mutato <xliff:g id="name">%s</xliff:g></string> <string name="unmuted_user"><xliff:g id="name">%s</xliff:g> riattivato</string>
<string name="unread_count">Conteggio elementi non letti</string> <string name="unread_count">Conteggio elementi non letti</string>
<string name="unsubscribe_from_user_list">Annulla l\'iscrizione alla lista <xliff:g id="name">%s</xliff:g></string> <string name="unsubscribe_from_user_list">Annulla l\'iscrizione alla lista <xliff:g id="name">%s</xliff:g></string>
<string name="unsubscribe_from_user_list_confirm_message">Annullare l\'iscrizione alla lista <xliff:g id="name">%s</xliff:g>? Potrai riscriverti in seguito.</string> <string name="unsubscribe_from_user_list_confirm_message">Annullare l\'iscrizione alla lista <xliff:g id="name">%s</xliff:g>? Potrai riscriverti in seguito.</string>
<string name="unsubscribed_from_list">Cancellato dalla lista \"<xliff:g id="list">%s</xliff:g>\".</string> <string name="unsubscribed_from_list">Rimosso dalla lista \"<xliff:g id="list">%s</xliff:g>\".</string>
<string name="unsupported_activity_action_summary">Per favore rettifica l\'azione qui sopra</string> <string name="unsupported_activity_action_summary">Per favore rettifica l\'azione qui sopra</string>
<string name="update_status">Invia tweet</string> <string name="update_status">Invia tweet</string>
<string name="updated_list_details">Dettagli della lista \"<xliff:g id="list">%s</xliff:g>\" aggiornati.</string> <string name="updated_list_details">Dettagli della lista \"<xliff:g id="list">%s</xliff:g>\" aggiornati.</string>

View File

@ -25,6 +25,40 @@
<item>true</item> <item>true</item>
</string-array> </string-array>
<string-array name="names_official_consumer_secret">
<!--Twitter for Android-->
<item>Twitter for Android</item>
<!--Twitter for iPhone-->
<item>Twitter for iPhone</item>
<!--Twitter for iPad-->
<item>Twitter for iPad</item>
<!--Twitter for Mac-->
<item>Twitter for Mac</item>
<!--Twitter for Windows Phone-->
<item>Twitter for Windows Phone</item>
<!--Twitter for Google TV-->
<item>Twitter for Google TV</item>
<!--TweetDeck-->
<item>TweetDeck</item>
</string-array>
<string-array name="types_official_consumer_secret">
<!--Twitter for Android-->
<item>TWITTER_FOR_ANDROID</item>
<!--Twitter for iPhone-->
<item>TWITTER_FOR_IPHONE</item>
<!--Twitter for iPad-->
<item>TWITTER_FOR_IPAD</item>
<!--Twitter for Mac-->
<item>TWITTER_FOR_MAC</item>
<!--Twitter for Windows Phone-->
<item>TWITTER_FOR_WINDOWS_PHONE</item>
<!--Twitter for Google TV-->
<item>TWITTER_FOR_GOOGLE_TV</item>
<!--TweetDeck-->
<item>TWEETDECK</item>
</string-array>
<string-array name="value_image_sources"> <string-array name="value_image_sources">
<item>camera</item> <item>camera</item>
<item>gallery</item> <item>gallery</item>
@ -65,6 +99,23 @@
<item>mentions</item> <item>mentions</item>
<item>inbox</item> <item>inbox</item>
</string-array> </string-array>
<!-- CRC32 checksum of consumer secret of official clients to check whether user is using official keys -->
<string-array name="values_official_consumer_secret_crc32">
<!--Twitter for Android-->
<item>6ce85096</item>
<!--Twitter for iPhone-->
<item>deffe9c7</item>
<!--Twitter for iPad-->
<item>9f00e0cb</item>
<!--Twitter for Mac-->
<item>df27640e</item>
<!--Twitter for Windows Phone-->
<item>62bd0d33</item>
<!--Twitter for Google TV-->
<item>56d8f9ff</item>
<!--TweetDeck-->
<item>ac602936</item>
</string-array>
<string-array name="values_profile_image_style"> <string-array name="values_profile_image_style">
<item>round</item> <item>round</item>
<item>square</item> <item>square</item>