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+
Twidere-Android is maintained by community and supporter including [Dimension](https://dimension.im/).
---
## Features ##

View File

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

View File

@ -41,5 +41,5 @@ public interface Twitter extends SearchResources, TimelineResources, TweetResour
ListResources, DirectMessagesResources, DirectMessagesEventResources,
FriendsFollowersResources, FavoritesResources, SpamReportingResources,
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.
*/
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
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.Parcelable;
import com.bluelinelabs.logansquare.annotation.JsonField;
import com.bluelinelabs.logansquare.annotation.JsonObject;
import com.hannesdorfmann.parcelableplease.annotation.ParcelablePlease;
import com.hannesdorfmann.parcelableplease.annotation.ParcelableThisPlease;
/**
* 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
public int describeContents() {
@ -59,6 +72,7 @@ public class TwitterAccountExtras implements Parcelable, AccountExtras {
@Override
public String toString() {
return "TwitterAccountExtras{" +
"officialCredentials=" + officialCredentials +
'}';
}
}

View File

@ -216,6 +216,7 @@ dependencies {
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.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 'androidx.exifinterface:exifinterface:1.1.0'
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.NFC"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Used for account management -->
<uses-permission

View File

@ -129,6 +129,26 @@ public class AccountUtils {
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) {
if (accountType == null) return R.drawable.ic_account_logo_twitter;
switch (accountType) {

View File

@ -2,6 +2,7 @@ package org.mariotaku.twidere.util;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.content.Context;
import android.text.TextUtils;
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.UrlEntity;
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.model.ConsumerKeyType;
import org.mariotaku.twidere.model.SpanItem;
import java.nio.charset.Charset;
import java.util.zip.CRC32;
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.account.cred.Credentials;
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.Locale;
@ -166,6 +169,21 @@ public class MicroBlogAPIFactory implements TwidereConstants {
@WorkerThread
@Nullable
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;
}

View File

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

View File

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

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.auth.BasicAuthorization
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.restfu.http.Endpoint
import org.mariotaku.restfu.oauth.OAuthToken
@ -410,7 +411,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
result.addAccount(am, preferences[randomizeAccountNameKey])
Analyzer.log(SignIn(true, accountType = result.type,
credentialsType = apiConfig.credentialsType,
officialKey = false))
officialKey = result.extras?.official == true))
finishSignIn()
}
}
@ -1220,7 +1221,17 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher,
}
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 {

View File

@ -453,7 +453,7 @@ abstract class ParcelableStatusesAdapter(
val timestamp = cursor.safeGetLong(indices[Statuses.TIMESTAMP])
val sortId = cursor.safeGetLong(indices[Statuses.SORT_ID])
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)
infoCache?.set(dataPosition, 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.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 {
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())
}
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 {
val accounts = AccountUtils.getAccounts(this)
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.twidere.TwidereConstants.DEFAULT_TWITTER_API_URL_FORMAT
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.model.ConsumerKeyType
import org.mariotaku.twidere.model.account.cred.*
import org.mariotaku.twidere.util.HttpClientFactory
import org.mariotaku.twidere.util.InternalTwitterContentUtils
@ -149,7 +148,9 @@ fun <T> newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authori
val factory = RestAPIFactory<MicroBlogException>()
val extraHeaders = run {
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))
val holder = DependencyHolder.get(context)
var extraRequestParams: Map<String, String>? = null

View File

@ -306,10 +306,13 @@ abstract class AbsActivitiesFragment protected constructor() :
override fun onGapClick(holder: GapViewHolder, position: Int) {
val activity = adapter.getActivity(position)
DebugLog.v(msg = "Load activity gap $activity")
if (activity.action !in Activity.Action.MENTION_ACTIONS) {
adapter.removeGapLoadingId(ObjectId(activity.account_key, activity.id))
adapter.notifyItemChanged(position)
return
if (!AccountUtils.isOfficial(context, activity.account_key)) {
// Skip if item is not a status
if (activity.action !in Activity.Action.MENTION_ACTIONS) {
adapter.removeGapLoadingId(ObjectId(activity.account_key, activity.id))
adapter.notifyItemChanged(position)
return
}
}
val accountKeys = arrayOf(activity.account_key)
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.TabAccountFlags
import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.Tab
import org.mariotaku.twidere.model.tab.DrawableHolder
@ -289,9 +290,9 @@ class CustomTabsFragment : BaseFragment(), LoaderCallbacks<Cursor?>, MultiChoice
if (!accountRequired) {
accountsAdapter.add(AccountDetails.dummy())
}
val officialKeyOnly = currentArguments.getBoolean(EXTRA_OFFICIAL_KEY_ONLY, false)
accountsAdapter.addAll(AccountUtils.getAllAccountDetails(AccountManager.get(currentContext), true).filter {
if (officialKeyOnly) {
val officialKeyOnly = arguments?.getBoolean(EXTRA_OFFICIAL_KEY_ONLY, false) ?: false
accountsAdapter.addAll(AccountUtils.getAllAccountDetails(AccountManager.get(context), true).filter {
if (officialKeyOnly && !it.isOfficial(context)) {
return@filter false
}
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.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterUpload
import org.mariotaku.pickncrop.library.MediaPickerActivity
import org.mariotaku.sqliteqb.library.Expression
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.util.AccountUtils
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.DestroyConversationTask
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?) {
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 -> {
when (resultCode) {
Activity.RESULT_OK -> {
@ -318,6 +327,19 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
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) {
ProgressDialogFragment.show(childFragmentManager, "set_notifications_disabled_progress")
val weakThis = WeakReference(this)
@ -361,6 +383,9 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
val context = fragment.context
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return@updateAction microBlog.updateDmConversationName(conversationId, name).isSuccessful
}
}
}
throw UnsupportedOperationException()
@ -370,6 +395,53 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
}
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(

View File

@ -44,6 +44,7 @@ import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.SelectableUsersAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.queryOne
import org.mariotaku.twidere.extension.text.appendCompat
import org.mariotaku.twidere.fragment.BaseFragment
@ -242,7 +243,11 @@ class MessageNewConversationFragment : BaseFragment(), LoaderCallbacks<List<Parc
val activity = activity ?: return
val selected = this.selectedRecipients
if (selected.isEmpty()) return
val maxParticipants = 1
val maxParticipants = if (account.isOfficial(context)) {
defaultFeatures.twitterDirectMessageMaxParticipants
} else {
1
}
if (selected.size > maxParticipants) {
editParticipants.error = getString(R.string.error_message_message_too_many_participants)
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 scrollListener: RecyclerViewScrollHandler<StatusDetailsAdapter>
private var loadTranslationTask: LoadTranslationTask? = null
// Data fields
private var conversationLoaderInitialized: Boolean = false
@ -505,9 +506,18 @@ class StatusFragment : BaseFragment(), LoaderCallbacks<SingleResponse<Parcelable
}
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() {
loadTranslationTask = null
loadTranslation(adapter.status)
}
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(
context: Context,
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.model.UserKey
import org.mariotaku.twidere.model.event.FriendshipTaskEvent
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.view.holder.UserViewHolder
class IncomingFriendshipsFragment : ParcelableUsersFragment(), IUsersAdapter.RequestClickListener {
@ -48,6 +49,8 @@ class IncomingFriendshipsFragment : ParcelableUsersFragment(), IUsersAdapter.Req
val accountKey = arguments?.getParcelable<UserKey?>(EXTRA_ACCOUNT_KEY) ?: return adapter
if (USER_TYPE_FANFOU_COM == accountKey.host) {
adapter.requestClickListener = this
} else if (AccountUtils.isOfficial(context, accountKey)) {
adapter.requestClickListener = this
}
return adapter
}

View File

@ -62,6 +62,6 @@ class DefaultAPIConfigLoader(context: Context) : FixedAsyncTaskLoader<List<Custo
}
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.model.api.mastodon.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.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
@ -92,6 +93,11 @@ class ConversationLoader(
// val isOfficial = account.isOfficial(context)
val isOfficial = false
canLoadAllReplies = isOfficial
if (isOfficial) {
return microBlog.showConversation(status.id, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
}
return showConversationCompat(microBlog, account, status, true)
}
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.SearchQuery
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.FilterScope
import org.mariotaku.twidere.extension.model.api.toParcelable
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.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
@ -76,6 +78,9 @@ open class MediaStatusesSearchLoader(
protected open fun processQuery(details: AccountDetails, query: String): String {
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
@ -87,6 +92,15 @@ open class MediaStatusesSearchLoader(
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
when (account.type) {
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)
searchQuery.paging(paging)
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.toParcelable
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.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
@ -89,23 +90,32 @@ class MediaTimelineLoader(
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
when (account.type) {
AccountType.TWITTER -> {
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
if (account.isOfficial(context)) {
if (userKey != null) {
return microBlog.getMediaTimeline(userKey.id, paging)
}
if (screenName != null) {
return microBlog.getMediaTimelineByScreenName(screenName, paging)
}
} 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")
}
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.toParcelable
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.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
@ -75,6 +76,9 @@ open class TweetSearchLoader(
protected open fun processQuery(details: AccountDetails, query: String): String {
if (details.type == AccountType.TWITTER) {
if (details.extras?.official == true) {
return smQuery(query, pagination)
}
return "$query exclude:retweets"
}
return query
@ -104,6 +108,15 @@ open class TweetSearchLoader(
val queryText = processQuery(account, query)
when (account.type) {
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)
searchQuery.paging(paging)
return microBlog.search(searchQuery)

View File

@ -21,6 +21,7 @@ package org.mariotaku.twidere.loader.statuses
import android.content.Context
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.official
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
@ -42,6 +43,9 @@ class UserMentionsLoader(
override fun processQuery(details: AccountDetails, query: String): String {
val screenName = query.substringAfter("@")
if (details.type == AccountType.TWITTER) {
if (details.extras?.official == true) {
return smQuery("to:$screenName", pagination)
}
return "to:$screenName exclude:retweets"
}
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.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.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableUser
@ -62,7 +63,9 @@ class StatusFavoritersLoader(
}
AccountType.TWITTER -> {
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 htmlUsers = web.getFavoritedPopup(statusId).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.constant.IntentConstants.EXTRA_MENTIONS_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.model.AccountDetails
import org.mariotaku.twidere.model.Tab
@ -104,11 +105,11 @@ class InteractionsTabConfiguration : TabConfiguration() {
val am = AccountManager.get(context)
val accounts = AccountUtils.getAllAccountDetails(am, false)
interactionsAvailable = accounts.any { it.supportsInteractions }
requiresStreaming = accounts.all { true }
requiresStreaming = accounts.all { it.requiresStreaming }
} else when (account.type) {
AccountType.TWITTER -> {
interactionsAvailable = true
requiresStreaming = true
requiresStreaming = !account.isOfficial(context)
}
AccountType.MASTODON -> {
interactionsAvailable = true
@ -160,7 +161,7 @@ class InteractionsTabConfiguration : TabConfiguration() {
get() = type == AccountType.TWITTER || type == AccountType.MASTODON
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<*>? {
when (account.type) {
AccountType.TWITTER -> {
return TwitterStreamingRunnable(this, account, preferences)
}
}
return null
}
@ -232,6 +237,236 @@ class StreamingService : BaseService() {
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 {
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.event.FriendshipTaskEvent
import org.mariotaku.twidere.util.Utils
import java.lang.UnsupportedOperationException
/**
* 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)
}
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.event.FriendshipTaskEvent
import org.mariotaku.twidere.util.Utils
import java.lang.UnsupportedOperationException
/**
* 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)
}
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
import android.app.Activity
import android.content.ContentValues
import android.media.MediaScannerConnection
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.CacheFileType
import org.mariotaku.twidere.provider.CacheProvider
import java.io.File
/**
@ -30,14 +36,53 @@ import java.io.File
*/
class SaveMediaToGalleryTask(
activity: Activity,
fileInfo: FileInfo,
private val fileInfo: FileInfo,
destination: File
) : ProgressSaveFileTask(activity, destination, fileInfo) {
override fun onFileSaved(savedFile: File, mimeType: String?) {
val context = context ?: return
MediaScannerConnection.scanFile(context, arrayOf(savedFile.path),
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()
}

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.microblog.toParcelable
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.fragment.InteractionsTimelineFragment
import org.mariotaku.twidere.model.AccountDetails
@ -81,6 +82,28 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
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 -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val activities = microBlog.getMentions(paging).map {

View File

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

View File

@ -244,7 +244,7 @@ abstract class GetStatusesTask(
if (result == null) return@forEach
val account = result.account
val task = CacheTimelineResultTask(context, result,
account.type == AccountType.STATUSNET)
account.type == AccountType.STATUSNET || account.isOfficial(context))
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.sqliteqb.library.Expression
import org.mariotaku.twidere.annotation.AccountType
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
@ -88,6 +89,9 @@ class DestroyConversationTask(
private fun requestDestroyConversation(microBlog: MicroBlog, account: AccountDetails): Boolean {
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return microBlog.deleteDmConversation(conversationId).isSuccessful
}
}
}
return false

View File

@ -25,6 +25,7 @@ import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.annotation.AccountType
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.UserKey
@ -74,6 +75,9 @@ class DestroyMessageTask(
account: AccountDetails, messageId: String): Boolean {
when (account.type) {
AccountType.TWITTER -> {
if (account.isOfficial(context)) {
return microBlog.destroyDm(messageId).isSuccessful
}
}
}
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
// 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
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 {
val conversationId = param.conversationId
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,
param: RefreshMessagesTaskParam, index: Int): DatabaseUpdateData {
val accountKey = details.key
@ -220,10 +265,16 @@ class GetMessagesTask(
defaultKeys, false)
val outgoingIds = DataStoreUtils.getNewestMessageIds(context, Messages.CONTENT_URI,
defaultKeys, true)
val cursors = DataStoreUtils.getNewestConversations(context, Conversations.CONTENT_URI,
twitterOfficialKeys).mapToArray { it?.request_cursor }
accounts.forEachIndexed { index, details ->
if (details == null) return@forEachIndexed
result[index] = SinceMaxPagination.sinceId(incomingIds[index], -1)
result[accounts.size + index] = SinceMaxPagination.sinceId(outgoingIds[index], -1)
if (details.isOfficial(context)) {
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
}
@ -240,7 +291,7 @@ class GetMessagesTask(
val outgoingIds = DataStoreUtils.getOldestMessageIds(context, Messages.CONTENT_URI,
defaultKeys, true)
val oldestConversations = DataStoreUtils.getOldestConversations(context,
Conversations.CONTENT_URI, emptyArray())
Conversations.CONTENT_URI, twitterOfficialKeys)
oldestConversations.forEachIndexed { i, conversation ->
val extras = conversation?.conversation_extras as? TwitterOfficialConversationExtras ?: return@forEachIndexed
incomingIds[i] = extras.maxEntryId
@ -281,6 +332,19 @@ class GetMessagesTask(
protected val defaultKeys: Array<UserKey?> by lazy {
return@lazy accounts.map { account ->
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
}.toTypedArray()
}

View File

@ -28,6 +28,7 @@ import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.sqliteqb.library.OrderBy
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.timestamp
import org.mariotaku.twidere.extension.queryOne
@ -80,6 +81,23 @@ class MarkMessageReadTask(
internal fun performMarkRead(context: Context, microBlog: MicroBlog, account: AccountDetails,
conversation: ParcelableMessageConversation): Pair<String, Long>? {
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
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.annotation.AccountType
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.model.AccountDetails
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.task.ExceptionHandlingAbstractTask
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.addLocalConversations
@ -84,7 +86,11 @@ class SendMessageTask(
message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData {
when (account.type) {
AccountType.TWITTER -> {
return sendTwitterMessageEvent(microBlog, account, message)
if (account.isOfficial(context)) {
return sendTwitterOfficialDM(microBlog, account, message)
} else {
return sendTwitterMessageEvent(microBlog, account, message)
}
}
AccountType.FANFOU -> {
return sendFanfouDM(microBlog, account, message)
@ -93,6 +99,47 @@ class SendMessageTask(
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,
message: ParcelableNewMessage): GetMessagesTask.DatabaseUpdateData {
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.MicroBlogException
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.notificationDisabled
import org.mariotaku.twidere.model.AccountDetails
@ -60,6 +61,24 @@ class SetConversationNotificationDisabledTask(
private fun requestSetNotificationDisabled(microBlog: MicroBlog, account: AccountDetails):
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,
conversationId) ?: return GetMessagesTask.DatabaseUpdateData(emptyList(), emptyList())
conversation.notificationDisabled = notificationDisabled

View File

@ -377,6 +377,18 @@ class AsyncTwitterWrapper(
}
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) {

View File

@ -998,8 +998,23 @@ object DataStoreUtils {
private fun <T> getOfficialSeparatedIds(context: Context, getFromDatabase: (Array<UserKey?>, Boolean) -> T,
mergeResult: (T, T) -> T, accountKeys: Array<UserKey?>): T {
val officialMaxPositions = getFromDatabase(emptyArray(), true)
val notOfficialMaxPositions = getFromDatabase(accountKeys, false)
val officialKeys = Array(accountKeys.size) {
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)
}
}

View File

@ -54,6 +54,7 @@ import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.constant.favoriteConfirmationKey
import org.mariotaku.twidere.constant.iWantMyStarsBackKey
import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.fragment.AbsStatusesFragment
import org.mariotaku.twidere.fragment.AddStatusFilterDialogFragment
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)
}
}
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)
addIntentToMenuForExtension(context, menu, MENU_GROUP_STATUS_EXTENSION,
INTENT_ACTION_EXTENSION_OPEN_STATUS, EXTRA_STATUS, EXTRA_STATUS_JSON, status)
@ -279,8 +284,10 @@ object MenuUtils {
DestroyStatusDialogFragment.show(fm, status)
}
R.id.pin -> {
PinStatusDialogFragment.show(fm, status)
}
R.id.unpin -> {
UnpinStatusDialogFragment.show(fm, status)
}
R.id.add_to_filter -> {
AddStatusFilterDialogFragment.show(fm, status)

View File

@ -89,6 +89,13 @@ class ComposeEditText(
} catch (e: AbstractMethodError) {
// http://crashes.to/s/69acd0ea0de
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
translateLabelView.setText(R.string.unknown_language)
translateContainer.visibility = View.GONE
if (CheckUtils.isValidLocale(lang) && account.isOfficial(context)) {
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)
translateResultView.setTextIsSelectable(true)

View File

@ -29,14 +29,14 @@
<!-- [verb] Edit image/settings etc. -->
<string name="action_edit">Edita</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>
<!-- [verb] Perform import action -->
<string name="action_import_from">Importa da&#8230;</string>
<!-- Used for decide something later, like permission request -->
<!-- [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_mute">Muto</string>
<string name="action_like">Mi piace</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_open_in_browser">Apri nel browser</string>
<string name="action_pick_color">Scegli colore</string>
@ -47,7 +47,7 @@
<!-- [verb] Restore purchase -->
<string name="action_retry">Riprova</string>
<!-- [verb] Action for performing retweet -->
<string name="action_retweet">Retweet</string>
<string name="action_retweet">Ritwitta</string>
<!-- [verb] Save settings/files etc. -->
<string name="action_save">Salva</string>
<string name="action_search">Cerca</string>
@ -63,17 +63,17 @@
<!-- [verb] Disconnect from network storage -->
<string name="action_sync_settings">Impostazioni</string>
<string name="action_take_photo">Scatta una foto</string>
<string name="action_toggle">Toggle</string>
<string name="action_translate">Traduce</string>
<string name="action_toggle">Apri</string>
<string name="action_translate">Traduci</string>
<string name="action_twitter_mute_user">Filtra utente</string>
<string name="action_twitter_muted_users">Utenti filtrati</string>
<!-- [verb] Action for unblocking user -->
<string name="action_unblock">Sblocca</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_unmute">Muto off</string>
<string name="action_unsubscribe">Cancellati</string>
<string name="action_unmute">Riattiva</string>
<string name="action_unsubscribe">Disiscriviti</string>
<string name="action_view_map">Mappa</string>
<string name="activated_accounts">Account attivati</string>
<string name="activities_about_me">Mie Attività</string>
@ -82,7 +82,7 @@
<string name="add_image">Aggiungi immagine</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="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. -->
<string name="app_name">Twidere</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_xauth">xAuth</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_summary">Disabilita l\'anteprima dei media su connessione a consumo</string>
<string name="belongs_to">Appartiene a</string>
@ -116,11 +116,11 @@
<string name="comment_hint">Commento&#8230;</string>
<string name="compact_cards">Schede compatte</string>
<string name="compact_cards_summary">Visualizza più schede sullo schermo</string>
<string name="compose_now">Compose Now</string>
<string name="compose_now_action">Azione Compose Now</string>
<string name="compose_now_summary">Sostituisce la scorciatoia a Google Now con la Compose screen</string>
<string name="compose_now">Componimento veloce</string>
<string name="compose_now_action">Azione Componimento veloce</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="connection_timeout">Timeout connessione</string>
<string name="connection_timeout">Timeout per la connessione</string>
<string name="consumer_key">Consumer Key</string>
<string name="consumer_secret">Consumer secret</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_summary">Queste impostazioni veranno applicate al prossimo login</string>
<string name="default_ringtone">Suoneria predefinita</string>
<string name="delete_conversation">Cancella conversazione</string>
<string name="delete_conversation_confirm_message">Cancellare tutti i messaggi di questa conversazione?</string>
<string name="delete_conversation">Elimina 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_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_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_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="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="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="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>
@ -200,19 +200,19 @@
<!-- Enhanced (paid) features title -->
<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="filter_type_keywords">Keywords</string>
<string name="filter_type_keywords">Parole chiave</string>
<string name="filter_type_links">Collegamenti</string>
<string name="filter_type_sources">Fonti</string>
<string name="filter_type_users">Utenti</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="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_you">Chi ti segue</string>
<string name="follows">Segui</string>
<string name="font">Carattere</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_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>
@ -227,8 +227,8 @@
<string name="hashtag">Hashtag</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_title">ATTENZIONE: queste opzioni possono far male!</string>
<string name="hide_card_actions">Nascondi azioni di carte</string>
<string name="hidden_settings_warning_title">ATTENZIONE: queste opzioni creare danni!</string>
<string name="hide_card_actions">Nascondi le azioni per il tweet</string>
<string name="hide_quotes">Nascondi citazioni</string>
<string name="hide_replies">Nascondi risposte</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_host">Host</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_restored_message">Icona ripristinata!</string>
<string name="import_export_settings">Importa/Esporta le impostazioni</string>
<string name="import_settings">Importa impostazioni</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="incoming_friendships">Richieste di seguirti in attesa</string>
<string name="input_text">Immissione testo</string>
<string name="incoming_friendships">Richieste di follow in attesa</string>
<string name="input_text">Immetti testo</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_list_name">Deve iniziare con una lettera e può contenere solo lettere, numeri, \"-\" o \"_\".</string>
<string name="invalid_tab">Tab non valida</string>
@ -261,9 +261,9 @@
<string name="keyboard_shortcut_back">Indietro</string>
<string name="keyboard_shortcut_hint">Premi i tasti</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_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/ -->
<!-- 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/ -->
@ -318,11 +318,11 @@
<string name="members">Membri</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_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="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_blocked_user">Bloccato <xliff:g id="user">%s</xliff:g>.</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"><xliff:g id="user">%s</xliff:g> bloccato.</string>
<string name="message_direct_message_deleted">Messaggio diretto cancellato.</string>
<string name="message_direct_message_sent">Messaggio diretto inviato.</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 -->
<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_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 -->
<string name="message_toast_no_account">Nessun account</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_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_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_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_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="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="muted_user">Muto on <xliff:g id="name">%s</xliff:g></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_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="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 Ritwittato</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_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_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="navigation">Navigazione</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_label_shorten_status">Riduci il tweet</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_message">L\'app richiede i seguenti permessi</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="photo">Foto</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="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_title_accounts">Accounts</string>
<string name="preference_title_accounts">Profili</string>
<string name="preference_title_advanced">Avanzate</string>
<string name="preference_title_database_item_limit">Limite dimensione database</string>
<string name="preference_title_landscape">Landscape</string>
@ -441,7 +441,7 @@
<string name="preference_title_storage">Archivio</string>
<string name="preference_title_streaming_enabled">Attiva streaming</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="preload_wifi_only">Precarica solo se in Wi-Fi</string>
<string name="preview">Anteprima</string>
@ -465,7 +465,7 @@
<string name="profile_text_color">Colore testo</string>
<string name="profile_updated">Profilo aggiornato.</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>
<!-- Normally you don't need to translate this -->
<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_to_default">Ripristina come da default</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_name">ReTweettato 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_count">Ritwittato da <xliff:g id="retweet_count">%d</xliff:g> utenti</string>
<string name="retweeted_by_name">Ritwittato da <xliff:g id="user_name">%s</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="revoke_permissions">Revoca permessi</string>
<string name="round">Tonda</string>
@ -526,9 +526,9 @@
<string name="save_to_gallery">Salva nella galleria</string>
<string name="saved_searches">Ricerche salvate</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="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_type_statuses">Tweet</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_sync_service">Servizio sincronizzazione timeline</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_block_user">Blocca <xliff:g id="name">%s</xliff:g></string>
<string name="title_blocked_users">Utenti bloccati</string>
@ -635,7 +635,7 @@
<string name="title_favorites">Preferiti</string>
<string name="title_filters">Filtri</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_home">Home</string>
<!-- [noun] Like, Formerly Twitter's favorite, in the plural -->
@ -654,17 +654,17 @@
<string name="title_user">Utenti</string>
<string name="title_user_colors">Colori personalizzati</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_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="translators">Traduttori</string>
<string name="trends">Trends</string>
<string name="trends_location">Località per i Trends</string>
<string name="trends_location_summary">Scegli la località per i trends.</string>
<string name="trends">Tendenze</string>
<string name="trends_location">Località per le tendenze</string>
<string name="trends_location_summary">Scegli la località per le tendenze.</string>
<!-- 'Tweet' here is a verb -->
<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="type_name_to_search">Digita un nome da cercare</string>
<string name="type_to_compose">Digita per comporre</string>
@ -676,12 +676,12 @@
<string name="uninstall">Disinstalla</string>
<string name="unknown_language">Lingua 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="unmuted_user">Mutato <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"><xliff:g id="name">%s</xliff:g> riattivato</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_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="update_status">Invia tweet</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>
</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">
<item>camera</item>
<item>gallery</item>
@ -65,6 +99,23 @@
<item>mentions</item>
<item>inbox</item>
</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">
<item>round</item>
<item>square</item>