diff --git a/README.md b/README.md index 7d32500a9..564a31893 100644 --- a/README.md +++ b/README.md @@ -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 ## diff --git a/build.gradle b/build.gradle index 06958c277..8962660e6 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/Twitter.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/Twitter.java index d3cc2c7dc..94b11e67c 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/Twitter.java +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/Twitter.java @@ -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 { } diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/TwitterPrivate.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/TwitterPrivate.java new file mode 100644 index 000000000..4414f99be --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/TwitterPrivate.java @@ -0,0 +1,35 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 { +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateAccountResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateAccountResources.java new file mode 100644 index 000000000..9e6a699af --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateAccountResources.java @@ -0,0 +1,37 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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; + +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateActivityResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateActivityResources.java new file mode 100644 index 000000000..92e13c820 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateActivityResources.java @@ -0,0 +1,52 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 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; + + +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateDirectMessagesResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateDirectMessagesResources.java new file mode 100644 index 000000000..a88e96bde --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateDirectMessagesResources.java @@ -0,0 +1,102 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateFriendsFollowersResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateFriendsFollowersResources.java new file mode 100644 index 000000000..82cbedbd7 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateFriendsFollowersResources.java @@ -0,0 +1,43 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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; + +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateMutesResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateMutesResources.java new file mode 100644 index 000000000..deb4e98dc --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateMutesResources.java @@ -0,0 +1,42 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 getMutesKeywordsList(@Query Paging paging) throws MicroBlogException; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateResources.java new file mode 100644 index 000000000..03a6f8c98 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateResources.java @@ -0,0 +1,23 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 { + +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateSearchResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateSearchResources.java new file mode 100644 index 000000000..750fd4dac --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateSearchResources.java @@ -0,0 +1,38 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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; + +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTimelineResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTimelineResources.java new file mode 100644 index 000000000..3b589dac1 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTimelineResources.java @@ -0,0 +1,39 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 getMediaTimeline(@Query("user_id") String userId, @Query Paging paging) throws MicroBlogException; + + @GET("/statuses/media_timeline.json") + ResponseList getMediaTimelineByScreenName(@Query("screen_name") String screenName, @Query Paging paging) throws MicroBlogException; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTweetResources.java b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTweetResources.java new file mode 100644 index 000000000..de7a033d4 --- /dev/null +++ b/twidere.component.common/src/main/java/org/mariotaku/microblog/library/twitter/api/PrivateTweetResources.java @@ -0,0 +1,45 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright 2012-2017 Mariotaku Lee + * + * 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 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; +} diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ConsumerKeyType.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ConsumerKeyType.java index 549db2714..944d023b0 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ConsumerKeyType.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/ConsumerKeyType.java @@ -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) { diff --git a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/TwitterAccountExtras.java b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/TwitterAccountExtras.java index 0e516b757..7eb204877 100644 --- a/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/TwitterAccountExtras.java +++ b/twidere.component.common/src/main/java/org/mariotaku/twidere/model/account/TwitterAccountExtras.java @@ -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 + '}'; } } diff --git a/twidere/build.gradle b/twidere/build.gradle index 9d80f9787..0de070290 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -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']}" diff --git a/twidere/src/main/AndroidManifest.xml b/twidere/src/main/AndroidManifest.xml index 0fba8b659..d013ecc65 100644 --- a/twidere/src/main/AndroidManifest.xml +++ b/twidere/src/main/AndroidManifest.xml @@ -58,6 +58,7 @@ + { - 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) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt index 32375b2d5..04efc0f8f 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt @@ -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 { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt index 899d31901..db73a7492 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableStatusesAdapter.kt @@ -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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountDetailsExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountDetailsExtensions.kt index 00ef776f2..92998a6aa 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountDetailsExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountDetailsExtensions.kt @@ -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 AccountDetails.newMicroBlogInstance(context: Context, cls: Class): T { return credentials.newMicroBlogInstance(context, type, cls) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountExtensions.kt index c0be5d256..930b3e128 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/AccountExtensions.kt @@ -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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt index 135c5e442..3a4b864be 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/extension/model/CredentialsExtensions.kt @@ -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 newMicroBlogInstance(context: Context, endpoint: Endpoint, auth: Authori val factory = RestAPIFactory() 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? = null diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt index f90216a0e..ac0ef2730 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/AbsActivitiesFragment.kt @@ -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, diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CustomTabsFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CustomTabsFragment.kt index 1631299c9..1bc4678fb 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CustomTabsFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/CustomTabsFragment.kt @@ -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, 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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageConversationInfoFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageConversationInfoFragment.kt index 6086b446e..e6b05aa4c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageConversationInfoFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageConversationInfoFragment.kt @@ -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(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? = 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 performUpdateInfo( diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageNewConversationFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageNewConversationFragment.kt index 63d320280..25480b318 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageNewConversationFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/message/MessageNewConversationFragment.kt @@ -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 maxParticipants) { editParticipants.error = getString(R.string.error_message_message_too_many_participants) return diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/PinStatusDialogFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/PinStatusDialogFragment.kt new file mode 100644 index 000000000..6ae94d09b --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/PinStatusDialogFragment.kt @@ -0,0 +1,55 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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 + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt index 058a157e2..691dcc39a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/StatusFragment.kt @@ -123,6 +123,7 @@ class StatusFragment : BaseFragment(), LoaderCallbacks + private var loadTranslationTask: LoadTranslationTask? = null // Data fields private var conversationLoaderInitialized: Boolean = false @@ -505,9 +506,18 @@ class StatusFragment : BaseFragment(), LoaderCallbacks?) { @@ -673,6 +683,37 @@ class StatusFragment : BaseFragment(), LoaderCallbacks(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, diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/UnpinStatusDialogFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/UnpinStatusDialogFragment.kt new file mode 100644 index 000000000..49171f021 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/status/UnpinStatusDialogFragment.kt @@ -0,0 +1,55 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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 + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/users/IncomingFriendshipsFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/users/IncomingFriendshipsFragment.kt index c28fa8977..50183a989 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/users/IncomingFriendshipsFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/users/IncomingFriendshipsFragment.kt @@ -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(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 } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/DefaultAPIConfigLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/DefaultAPIConfigLoader.kt index 4357d11b9..78061f709 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/DefaultAPIConfigLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/DefaultAPIConfigLoader.kt @@ -62,6 +62,6 @@ class DefaultAPIConfigLoader(context: Context) : FixedAsyncTaskLoader { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaStatusesSearchLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaStatusesSearchLoader.kt index 26077f5fc..7e0ef7027 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaStatusesSearchLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaStatusesSearchLoader.kt @@ -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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaTimelineLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaTimelineLoader.kt index 359c644d1..f432a39b4 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaTimelineLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/MediaTimelineLoader.kt @@ -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() + 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() - 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 -> { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/TweetSearchLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/TweetSearchLoader.kt index bafec138d..f91b735f9 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/TweetSearchLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/TweetSearchLoader.kt @@ -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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/UserMentionsLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/UserMentionsLoader.kt index e716e559d..e8075a7f0 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/UserMentionsLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/statuses/UserMentionsLoader.kt @@ -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" diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/users/StatusFavoritersLoader.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/users/StatusFavoritersLoader.kt index 7e99ae2be..5054298b4 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/loader/users/StatusFavoritersLoader.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/loader/users/StatusFavoritersLoader.kt @@ -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)) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/model/tab/impl/InteractionsTabConfiguration.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/model/tab/impl/InteractionsTabConfiguration.kt index 8c6890acd..050e5158c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/model/tab/impl/InteractionsTabConfiguration.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/model/tab/impl/InteractionsTabConfiguration.kt @@ -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) } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt index 885631828..8c1a7716d 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt @@ -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(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 = 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 = 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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/AcceptFriendshipTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/AcceptFriendshipTask.kt index 40580b5f7..6202d9394 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/AcceptFriendshipTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/AcceptFriendshipTask.kt @@ -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) } } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/DenyFriendshipTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/DenyFriendshipTask.kt index 81e7492f4..b7b8f2e2b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/DenyFriendshipTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/DenyFriendshipTask.kt @@ -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) } } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/SaveMediaToGalleryTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/SaveMediaToGalleryTask.kt index 75f44a444..f1f5b1c7b 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/SaveMediaToGalleryTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/SaveMediaToGalleryTask.kt @@ -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() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/PinStatusTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/PinStatusTask.kt new file mode 100644 index 000000000..49e1b2dcf --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/PinStatusTask.kt @@ -0,0 +1,52 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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(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)) + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/UnpinStatusTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/UnpinStatusTask.kt new file mode 100644 index 000000000..0bcc94aa7 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/status/UnpinStatusTask.kt @@ -0,0 +1,52 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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(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)) + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesAboutMeTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesAboutMeTask.kt index eead73e86..41901ef6a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesAboutMeTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesAboutMeTask.kt @@ -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() + 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 { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt index 00642782c..6703722d8 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetActivitiesTask.kt @@ -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) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetStatusesTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetStatusesTask.kt index cb96ba1a6..458c041c1 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetStatusesTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/GetStatusesTask.kt @@ -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) } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/AddParticipantsTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/AddParticipantsTask.kt new file mode 100644 index 000000000..8c8983e7f --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/AddParticipantsTask.kt @@ -0,0 +1,97 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2017 Mariotaku Lee + * + * 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 . + */ + +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 +) : ExceptionHandlingAbstractTask 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") + } + +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyConversationTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyConversationTask.kt index 38975aee1..0dc989fe2 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyConversationTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyConversationTask.kt @@ -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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyMessageTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyMessageTask.kt index 7c6fbeb4c..dd0779e6a 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyMessageTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/DestroyMessageTask.kt @@ -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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/GetMessagesTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/GetMessagesTask.kt index 67ffc68b5..b79fbb405 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/GetMessagesTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/GetMessagesTask.kt @@ -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 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 by lazy { + return@lazy accounts.map { account -> + account ?: return@map null + if (!account.isOfficial(context)) { + return@map null + } return@map account.key }.toTypedArray() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/MarkMessageReadTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/MarkMessageReadTask.kt index ff1d398a2..5cd678c6c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/MarkMessageReadTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/MarkMessageReadTask.kt @@ -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? { 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) } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SendMessageTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SendMessageTask.kt index e4f38be0b..9f9095913 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SendMessageTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SendMessageTask.kt @@ -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? = null + var deleteAlways: List? = 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") diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SetConversationNotificationDisabledTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SetConversationNotificationDisabledTask.kt index b8bbc7b85..0239b4c20 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SetConversationNotificationDisabledTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/message/SetConversationNotificationDisabledTask.kt @@ -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 diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt index 740627656..5d38d5134 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt @@ -377,6 +377,18 @@ class AsyncTwitterWrapper( } fun setActivitiesAboutMeUnreadAsync(accountKeys: Array, cursor: Long) { + val task = object : ExceptionHandlingAbstractTask(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) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt index 02c63c229..dd6a742e8 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/DataStoreUtils.kt @@ -998,8 +998,23 @@ object DataStoreUtils { private fun getOfficialSeparatedIds(context: Context, getFromDatabase: (Array, Boolean) -> T, mergeResult: (T, T) -> T, accountKeys: Array): 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) } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt index e6b678b73..9800a6f40 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/MenuUtils.kt @@ -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) diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/view/ComposeEditText.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/view/ComposeEditText.kt index 5ba1b5c9e..7b64219dc 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/view/ComposeEditText.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/view/ComposeEditText.kt @@ -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 } } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt index 883813851..55c43b55d 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/view/holder/status/DetailStatusViewHolder.kt @@ -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) diff --git a/twidere/src/main/res-localized/values-it/strings.xml b/twidere/src/main/res-localized/values-it/strings.xml index 2f9347259..3d2cbc235 100644 --- a/twidere/src/main/res-localized/values-it/strings.xml +++ b/twidere/src/main/res-localized/values-it/strings.xml @@ -29,14 +29,14 @@ Edita Aggiungi ai preferiti - Finish + Fine Segui Importa da… - Like - Muto + Mi piace + Silenzia %1$s, salvato alle %2$s Apri nel browser Scegli colore @@ -47,7 +47,7 @@ Riprova - Retweet + Ritwitta Salva Cerca @@ -63,17 +63,17 @@ Impostazioni Scatta una foto - Toggle - Traduce + Apri + Traduci Filtra utente Utenti filtrati Sblocca Annulla like - Togli dai preferiti + Rimuovi dai preferiti Smetti di seguire - Muto off - Cancellati + Riattiva + Disiscriviti Mappa Account attivati Mie Attività @@ -82,7 +82,7 @@ Aggiungi immagine e altri %d Formato API URL - La tua Twitter app + La tua app per Twitter Twidere Twidere si riavvierà per rendere effettive le nuove le impostazioni. @@ -92,7 +92,7 @@ Modalità twip O xAuth Auto refresh - Background + Sfondo Modalità risparmio dati Disabilita l\'anteprima dei media su connessione a consumo Appartiene a @@ -116,11 +116,11 @@ Commento… Schede compatte Visualizza più schede sullo schermo - Compose Now - Azione Compose Now - Sostituisce la scorciatoia a Google Now con la Compose screen + Componimento veloce + Azione Componimento veloce + Sostituisce la scorciatoia a Google Now con la schemata di composizione Confitti con %s - Timeout connessione + Timeout per la connessione Consumer Key Consumer secret Contenuto @@ -141,19 +141,19 @@ Impostazioni di default API Queste impostazioni veranno applicate al prossimo login Suoneria predefinita - Cancella conversazione - Cancellare tutti i messaggi di questa conversazione? + Elimina conversazione + Eliminare tutti i messaggi di questa conversazione? Eliminare le bozze selezionate? Cancellare questo messaggio? Elimina utente %s Eliminare %s? Non è reversibile. Rimuovere %1$s dalla lista "%2$s\"? Elimina lista %s - Eliminare lisra %s? Non potrà essere annullato. + Eliminare lista %s? Non potrà essere annullato. Elimina utenti - Lista cancellata \"%s\". + La lista \"%s\" è stata eliminata. Rimosso %1$s dalla lista "%2$s\". - Richiesta %s di follow negata. + Richiesta di follow di %s negata. Rifiuta Elimina ricerca salvata \"%s\" Eliminare la ricerca \"%s\"? È possibile salvarla nuovamente in seguito. @@ -200,19 +200,19 @@ Caricamento veloce immagini Seleziona per far caricare più velocemente le immagini, disattivalo se alcune immagini non sono mostrate correttamente. - Keywords + Parole chiave Collegamenti Fonti Utenti Richiesta di follow inviata Seguito %s. - I tuoi following + Persone che segui Mostra notifiche solo dagli utenti che segui. Chi ti segue Segui Carattere Font - Da Camera + Da fotocamera Da galleria Da %1$s Da %1$s e %2$d altri @@ -227,8 +227,8 @@ Hashtag Impostazioni nascoste 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 - ATTENZIONE: queste opzioni possono far male! - Nascondi azioni di carte + ATTENZIONE: queste opzioni creare danni! + Nascondi le azioni per il tweet Nascondi citazioni Nascondi risposte Nascondi retweets @@ -241,18 +241,18 @@ Indirizzo (può essere un altro indirizzo host) Host Rivoglio le mie stelline! - Usa i preferiti (★) piuttosto che i like (♥︎) + Usa i preferiti (★) al posto dei mi piace (♥︎) Icona Icona ripristinata! Importa/Esporta le impostazioni Importa impostazioni Importa impostazioni… - Risposta a %s + Rispondi a %s Inbox - Richieste di seguirti in attesa - Immissione testo + Richieste di follow in attesa + Immetti testo Interazioni - Chaive da consumatore invalida + Chiave da consumatore invalida Segreto consumatore invalido Deve iniziare con una lettera e può contenere solo lettere, numeri, \"-\" o \"_\". Tab non valida @@ -261,9 +261,9 @@ Indietro Premi i tasti Scorciatoie da tastiera - Keyword: %s + Parola chiave: %s Tipo di autenticazione - Background operation service + Servizio di operazione in background @@ -318,11 +318,11 @@ Membri Menziona questo utente Menziona %s - Mention %1$s + Menziona %1$s Solo menzioni Dati delle API corrotti. - [DOMAIN]: Twitter API domain.\nPer Esempio: https://[DOMAIN].twitter.com/ sarà sostituito da https://api.twitter.com/. - Bloccato %s. + [DOMAIN]: Dominio dell\' API di Twitter.\nPer Esempio: https://[DOMAIN].twitter.com/ sarà sostituito da https://api.twitter.com/. + %s bloccato. Messaggio diretto cancellato. Messaggio diretto inviato. Attendere prego. @@ -336,30 +336,30 @@ Link copiato negli appunti Ecco un regalino per te, trovalo tra le impostazioni di sistema :) - Verifica di login fallita. + Verifica del login fallita. Nessun account Nessun account selezionato. Premi di nuovo per chiudere Immagine dell\'intestazione profilo aggiornata. Permesso di memoria richiesto per salvare questo media. - Salvato in Galleria. + Salvato nella Galleria. Permessi di memoria richiesti per selezionare il file. Alcune applicazioni richiedono permessi per condividere alcuni media. - Tweet salvato in bozze. + Tweet salvato nelle bozze. Tweet inviato. URL delle API non corretto, o consumer key/secret errata. Per favore, controllale. Selezione multipla - Muto %s + Silenzia %s Silenziare %s? Non vedrai più i tweets di questo utente ma continuerai a seguirlo. - Muto on %s - %1$s e un altro hanno ReTweettato - Retwittato da %1$s e da altri %2$d + %s silenziato + %1$s e un altro hanno Ritwittato + Ritwittato da %1$s e da altri %2$d Mostra il nome prima - Display \@nomeschermo primo + Mostra \@nomeschermo per primo Visualizza prima il nome %s non settato - ReTweettato da %s + Ritwittato da %s %1$s (%2$s) Navigazione Rete @@ -420,7 +420,7 @@ Scrittura sul database, aggiornamento degli stati Riduci il tweet Sincronizza timeline - Upload media + Carica media Richiesta di permessi L\'app richiede i seguenti permessi Avviso di rischio phishing @@ -429,11 +429,11 @@ Apre un avvertimento quando stai per aprire un link potenzialmente pericoloso in un DM. Foto Seleziona file - Play + Riproduci %1$s · %2$s Interrompi auto-aggiornamento quando la batteria è scarica Limite massimo degli elementi memorizzati nel database per ogni account, impostare su un valore inferiore per risparmiare spazio e aumentare la velocità di caricamento. - Accounts + Profili Avanzate Limite dimensione database Landscape @@ -441,7 +441,7 @@ Archivio Attiva streaming Dimensione del testo - Traduce + Traduci Località per i Trends Precarica solo se in Wi-Fi Anteprima @@ -465,7 +465,7 @@ Colore testo Profilo aggiornato. Avanzamento - Project account + Account del progetto Progetti di cui siamo parte Twidere @@ -515,9 +515,9 @@ Resettare le scorciatoie da tastiera ai valori di default? Ripristina come da default Riprova se si verifica un errore di rete - ReTweettato da %d utenti - ReTweettato da %s - ReTweettato da %1$s e altri %2$d + Ritwittato da %d utenti + Ritwittato da %s + Ritwittato da %1$s e altri %2$d I miei retweets Revoca permessi Tonda @@ -526,9 +526,9 @@ Salva nella galleria Ricerche salvate Forse hai già salvato questa ricerca - Tweet schedulati + Tweet programmati Discarica - Cerca tweets o utenti + Cerca tweet o utenti Ricerca tweet Tweet Utenti @@ -619,7 +619,7 @@ Timeline di streaming in esecuzione Servizio sincronizzazione timeline Informazioni - Accounts + Profili Aggiungi o rimuovi dalla lista Blocca %s Utenti bloccati @@ -635,7 +635,7 @@ Preferiti Filtri URL - Followers + Follower Stai seguendo Home @@ -654,17 +654,17 @@ Utenti Colori personalizzati Appartiene a - Users che hanno inserito tra i preferiti + Utenti che hanno inserito questo elemento tra i preferiti Utenti a cui piace - Utenti che hanno retwittato questo + Utenti che hanno retwittato questo elemento Lingua Traduttori - Trends - Località per i Trends - Scegli la località per i trends. + Tendenze + Località per le tendenze + Scegli la località per le tendenze. Tweet da %1$s - Tweet #%1$s + Twitta #%1$s Twidere test Digita un nome da cercare Digita per comporre @@ -676,12 +676,12 @@ Disinstalla Lingua sconosciuta Posizione sconosciuta - Muto off %s - Mutato %s + Riattiva %s + %s riattivato Conteggio elementi non letti Annulla l\'iscrizione alla lista %s Annullare l\'iscrizione alla lista %s? Potrai riscriverti in seguito. - Cancellato dalla lista \"%s\". + Rimosso dalla lista \"%s\". Per favore rettifica l\'azione qui sopra Invia tweet Dettagli della lista \"%s\" aggiornati. diff --git a/twidere/src/main/res/values/arrays_no_translate.xml b/twidere/src/main/res/values/arrays_no_translate.xml index c5a1030bc..70f30d522 100644 --- a/twidere/src/main/res/values/arrays_no_translate.xml +++ b/twidere/src/main/res/values/arrays_no_translate.xml @@ -25,6 +25,40 @@ true + + + Twitter for Android + + Twitter for iPhone + + Twitter for iPad + + Twitter for Mac + + Twitter for Windows Phone + + Twitter for Google TV + + TweetDeck + + + + + TWITTER_FOR_ANDROID + + TWITTER_FOR_IPHONE + + TWITTER_FOR_IPAD + + TWITTER_FOR_MAC + + TWITTER_FOR_WINDOWS_PHONE + + TWITTER_FOR_GOOGLE_TV + + TWEETDECK + + camera gallery @@ -65,6 +99,23 @@ mentions inbox + + + + 6ce85096 + + deffe9c7 + + 9f00e0cb + + df27640e + + 62bd0d33 + + 56d8f9ff + + ac602936 + round square