├── .gitignore ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── apk.sh ├── build.gradle ├── proguard-rules.pro ├── src │ ├── androidTest │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── io │ │ │ └── kaif │ │ │ └── mobile │ │ │ ├── TestBeans.java │ │ │ ├── kmark │ │ │ └── KmarkProcessorTest.java │ │ │ ├── retrofit │ │ │ └── RetrofitRetryStaleProxyTest.java │ │ │ ├── test │ │ │ └── ModelFixture.java │ │ │ └── view │ │ │ ├── HomeActivityTest.java │ │ │ ├── LoginActivityTest.java │ │ │ └── daemon │ │ │ ├── ArticleDaemonTest.java │ │ │ ├── DebateDaemonTest.java │ │ │ ├── MockDaemonModule.java │ │ │ └── VoteDaemonTest.java │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── io │ │ │ └── kaif │ │ │ └── mobile │ │ │ ├── ApplicationTest.java │ │ │ ├── Beans.java │ │ │ ├── IgnoreAllSubscriber.java │ │ │ ├── KaifApplication.java │ │ │ ├── app │ │ │ ├── BaseActivity.java │ │ │ └── BaseFragment.java │ │ │ ├── config │ │ │ └── ApiConfiguration.java │ │ │ ├── event │ │ │ ├── EventPublishSubject.java │ │ │ ├── account │ │ │ │ ├── AccountEvent.java │ │ │ │ ├── SignInSuccessEvent.java │ │ │ │ └── SignOutEvent.java │ │ │ ├── article │ │ │ │ └── ArticleEvent.java │ │ │ ├── debate │ │ │ │ ├── CreateDebateFailedEvent.java │ │ │ │ ├── CreateDebateSuccessEvent.java │ │ │ │ ├── CreateLocalDebateEvent.java │ │ │ │ └── DebateEvent.java │ │ │ └── vote │ │ │ │ ├── VoteArticleSuccessEvent.java │ │ │ │ ├── VoteDebateSuccessEvent.java │ │ │ │ └── VoteEvent.java │ │ │ ├── json │ │ │ └── ApiResponseDeserializer.java │ │ │ ├── kmark │ │ │ ├── Block.java │ │ │ ├── BlockType.java │ │ │ ├── Configuration.java │ │ │ ├── Decorator.java │ │ │ ├── DefaultDecorator.java │ │ │ ├── Emitter.java │ │ │ ├── KmarkProcessor.java │ │ │ ├── Line.java │ │ │ ├── LineType.java │ │ │ ├── LinkRef.java │ │ │ ├── MarkToken.java │ │ │ ├── Utils.java │ │ │ └── text │ │ │ │ ├── BulletSpan2.java │ │ │ │ ├── CodeBlockSpan.java │ │ │ │ └── SuperscriptSpan2.java │ │ │ ├── model │ │ │ ├── Article.java │ │ │ ├── Debate.java │ │ │ ├── DebateNode.java │ │ │ ├── FeedAsset.java │ │ │ ├── LocalDebate.java │ │ │ ├── Vote.java │ │ │ ├── Zone.java │ │ │ ├── exception │ │ │ │ ├── DomainException.java │ │ │ │ └── DuplicateArticleUrlException.java │ │ │ └── oauth │ │ │ │ ├── AccessTokenInfo.java │ │ │ │ └── AccessTokenManager.java │ │ │ ├── retrofit │ │ │ ├── MethodInfo.java │ │ │ ├── RetrofitRetryStaleProxy.java │ │ │ └── RetryStaleHandler.java │ │ │ ├── service │ │ │ ├── AccountService.java │ │ │ ├── ArticleService.java │ │ │ ├── CommaSeparatedParam.java │ │ │ ├── DebateService.java │ │ │ ├── FeedService.java │ │ │ ├── OauthService.java │ │ │ ├── ServiceModule.java │ │ │ ├── VoteService.java │ │ │ └── ZoneService.java │ │ │ ├── util │ │ │ ├── StringUtils.java │ │ │ └── UtilModule.java │ │ │ └── view │ │ │ ├── ArticleListAdapter.java │ │ │ ├── ArticlesFragment.java │ │ │ ├── DebateListAdapter.java │ │ │ ├── DebatesActivity.java │ │ │ ├── DebatesFragment.java │ │ │ ├── HomeActivity.java │ │ │ ├── HomeFragment.java │ │ │ ├── HomePagerAdapter.java │ │ │ ├── LatestDebateListAdapter.java │ │ │ ├── LatestDebatesFragment.java │ │ │ ├── LoginActivity.java │ │ │ ├── NewsFeedActivity.java │ │ │ ├── NewsFeedActivityFragment.java │ │ │ ├── NewsFeedListAdapter.java │ │ │ ├── animation │ │ │ └── VoteAnimation.java │ │ │ ├── daemon │ │ │ ├── AccountDaemon.java │ │ │ ├── ArticleDaemon.java │ │ │ ├── DebateDaemon.java │ │ │ ├── FeedDaemon.java │ │ │ ├── VoteDaemon.java │ │ │ └── ZoneDaemon.java │ │ │ ├── drawable │ │ │ └── NewsFeedBadgeDrawable.java │ │ │ ├── graphics │ │ │ └── drawable │ │ │ │ ├── LevelDrawable.java │ │ │ │ └── Triangle.java │ │ │ ├── share │ │ │ ├── ShareArticleActivity.java │ │ │ └── ShareExternalLinkFragment.java │ │ │ ├── util │ │ │ └── Views.java │ │ │ ├── viewmodel │ │ │ ├── ArticleViewModel.java │ │ │ ├── DebateViewModel.java │ │ │ └── FeedAssetViewModel.java │ │ │ └── widget │ │ │ ├── ArticleScoreTextView.java │ │ │ ├── ClickableSpanTouchListener.java │ │ │ ├── DebateActions.java │ │ │ ├── OnScrollToLastListener.java │ │ │ ├── OnVoteClickListener.java │ │ │ ├── ReplyDialog.java │ │ │ └── VoteArticleButton.java │ │ ├── kotlin │ │ └── io │ │ │ └── kaif │ │ │ └── mobile │ │ │ └── view │ │ │ ├── HonorActivity.kt │ │ │ ├── HonorAdapter.kt │ │ │ └── HonorFragment.kt │ │ └── res │ │ ├── anim │ │ ├── rotate_vote.xml │ │ ├── rotate_vote_back.xml │ │ └── scale_action_icon.xml │ │ ├── drawable-hdpi │ │ ├── ic_notifications_white.png │ │ ├── ic_open_in_browser.png │ │ ├── ic_record_voice_over_white.png │ │ ├── ic_reply.png │ │ └── ic_send.png │ │ ├── drawable-xhdpi │ │ ├── ic_notifications_white.png │ │ ├── ic_open_in_browser.png │ │ ├── ic_record_voice_over_white.png │ │ ├── ic_reply.png │ │ └── ic_send.png │ │ ├── drawable-xxhdpi │ │ ├── ic_notifications_white.png │ │ ├── ic_open_in_browser.png │ │ ├── ic_record_voice_over_white.png │ │ ├── ic_reply.png │ │ └── ic_send.png │ │ ├── drawable │ │ ├── news_feed_divider.xml │ │ ├── news_feed_divider_default.xml │ │ ├── news_feed_divider_new.xml │ │ └── score_border.xml │ │ ├── layout │ │ ├── activity_debates.xml │ │ ├── activity_home.xml │ │ ├── activity_login.xml │ │ ├── activity_news_feed.xml │ │ ├── activity_share_article.xml │ │ ├── fragment_articles.xml │ │ ├── fragment_debates.xml │ │ ├── fragment_home.xml │ │ ├── fragment_latest_debates.xml │ │ ├── fragment_news_feed.xml │ │ ├── fragment_reply.xml │ │ ├── fragment_share_external_link.xml │ │ ├── item_article.xml │ │ ├── item_article_full.xml │ │ ├── item_debate.xml │ │ ├── item_debate_feed.xml │ │ ├── item_debate_header.xml │ │ ├── item_debate_latest.xml │ │ └── item_loading.xml │ │ ├── menu │ │ ├── menu_debates.xml │ │ ├── menu_home.xml │ │ └── menu_share.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values-zh │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── settings.xml │ │ ├── strings.xml │ │ └── themes.xml └── version.properties ├── build.gradle ├── gradle.properties ├── gradle ├── .gitignore └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── retry-stale-processor ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── io │ │ └── kaif │ │ └── mobile │ │ └── retrofit │ │ └── processor │ │ ├── AnnotationSpecUtil.java │ │ ├── RetrofitServiceInterface.java │ │ ├── RetrofitServiceMethod.java │ │ └── RetrofitServiceProcessor.java │ └── test │ └── java │ └── io │ └── kaif │ └── mobile │ └── retrofit │ └── processor │ ├── RetrofitServiceMethodTest.java │ └── RetrofitServiceProcessorTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | secret/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### io.kaif 2 | 3 | # Development 4 | 5 | ## Build Variant 6 | 7 | * Debug & Production will use real kaif.io, 8 | 9 | ## Prepare secret file 10 | 11 | To access resource from kaif.io(please check https://kaif.io/developer/doc ), you need to create app to get client secret and client id, fill it in api.properties for api accessing. 12 | 13 | 1. Create api.properties in `kaif-android/secret/api.properties` 14 | 15 | ``` 16 | CLIENT_ID=[your client id] 17 | CLIENT_SECRET=[your client secret] 18 | ``` 19 | 20 | 2. Edit `app/src/main/res/values/settings.xml`, change content of redirect_uri to your app's setting 21 | 22 | ``` 23 | [full redirect uri] 24 | [host of redirect uri] 25 | [path of redirect uri] 26 | [scheme of redirect uri] 27 | ``` 28 | 29 | # Packaging 30 | ``` 31 | kaif-android/secret 32 | ``` 33 | 34 | 1. Generate keystore file and move to `kaif-android/secret/kaif-keystore.jks` 35 | 2. Create `kaif-android/secret/password.properties` 36 | 37 | ``` 38 | KEY_STORE_PASSWORD=[your key store password] 39 | RELEASE_KEY_PASSWORD=[your release key password] 40 | ``` 41 | 42 | * NEVER commit `kaif-keystore.jks` and `password.properties` to git !!! See kaif-android/.gitignore 43 | 44 | 3. You can execute apk.sh to generate release apk 45 | `./apk.sh ~/Desktop/` 46 | 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /src/local -------------------------------------------------------------------------------- /app/apk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | apks="build/outputs/apk/app-debug-1.0.*.apk" 3 | 4 | function build { 5 | assembleType=$1 6 | target=$2 7 | ../gradlew --rerun-tasks clean $assembleType 8 | 9 | chmod 600 $apks 10 | cp $apks "`dirname $target`/`basename $target`/" 11 | } 12 | 13 | apks="build/outputs/apk/app-debug-1.0.*.apk" 14 | build assembleDebug $1 15 | 16 | apks="build/outputs/apk/app-release-1.0.*.apk" 17 | build assembleRelease $1 18 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/kojilin/development/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | #butterknife 20 | -keep class butterknife.** { *; } 21 | -dontwarn butterknife.internal.** 22 | -keep class **$$ViewInjector { *; } 23 | 24 | -keepclasseswithmembernames class * { 25 | @butterknife.* ; 26 | } 27 | 28 | -keepclasseswithmembernames class * { 29 | @butterknife.* ; 30 | } 31 | 32 | #retrolambda 33 | -dontwarn java.lang.invoke.* -------------------------------------------------------------------------------- /app/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/TestBeans.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import dagger.Component; 6 | import io.kaif.mobile.view.HomeActivityTest; 7 | import io.kaif.mobile.view.LoginActivityTest; 8 | import io.kaif.mobile.view.daemon.MockDaemonModule; 9 | 10 | @Singleton 11 | @Component(modules = MockDaemonModule.class) 12 | public interface TestBeans extends Beans { 13 | 14 | void inject(HomeActivityTest homeActivityTest); 15 | 16 | void inject(LoginActivityTest loginActivityTest); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/kmark/KmarkProcessorTest.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.kmark; 2 | 3 | import android.test.AndroidTestCase; 4 | import android.text.SpannableStringBuilder; 5 | import android.text.style.QuoteSpan; 6 | import android.text.style.StyleSpan; 7 | 8 | public class KmarkProcessorTest extends AndroidTestCase { 9 | 10 | public void testNestSpan_order_as_begin() { 11 | SpannableStringBuilder result = (SpannableStringBuilder) KmarkProcessor.process(getContext(), 12 | "> *Sample* text"); 13 | Object[] spans = result.getSpans(0, result.length(), Object.class); 14 | assertEquals(QuoteSpan.class, spans[0].getClass()); 15 | assertEquals(StyleSpan.class, spans[1].getClass()); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/test/ModelFixture.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.test; 2 | 3 | import java.util.Date; 4 | 5 | import io.kaif.mobile.model.Article; 6 | import io.kaif.mobile.model.Debate; 7 | import io.kaif.mobile.model.Vote; 8 | 9 | public interface ModelFixture { 10 | 11 | default Article article(String id) { 12 | return new Article("programming", 13 | "pro", 14 | id, 15 | "aTitle", 16 | new Date(), 17 | "http://foo.com", 18 | "content", 19 | Article.ArticleType.EXTERNAL_LINK, 20 | "bar", 21 | 0L, 22 | 0L); 23 | } 24 | 25 | default Vote upVote(String id) { 26 | return new Vote(id, Vote.VoteState.UP, new Date()); 27 | } 28 | 29 | default Vote downVote(String id) { 30 | return new Vote(id, Vote.VoteState.DOWN, new Date()); 31 | } 32 | 33 | default Vote emptyVote(String id) { 34 | return new Vote(id, Vote.VoteState.EMPTY, new Date()); 35 | } 36 | 37 | default Debate debate(String articleId, String debateId, String parentDebateId, int level) { 38 | return new Debate(articleId, 39 | debateId, 40 | "programming", 41 | parentDebateId, 42 | level, 43 | "content", 44 | "tester", 45 | 0L, 46 | 0L, 47 | new Date(), 48 | new Date()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/view/HomeActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.app.Instrumentation; 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.espresso.intent.Intents; 7 | import android.support.test.espresso.intent.matcher.IntentMatchers; 8 | import android.support.test.rule.ActivityTestRule; 9 | import android.support.test.runner.AndroidJUnit4; 10 | 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mockito; 17 | 18 | import javax.inject.Inject; 19 | 20 | import io.kaif.mobile.DaggerTestBeans; 21 | import io.kaif.mobile.KaifApplication; 22 | import io.kaif.mobile.TestBeans; 23 | import io.kaif.mobile.view.daemon.AccountDaemon; 24 | 25 | import static android.support.test.espresso.intent.Intents.intended; 26 | 27 | @RunWith(AndroidJUnit4.class) 28 | public class HomeActivityTest { 29 | 30 | @Inject 31 | AccountDaemon accountDaemon; 32 | 33 | @Rule 34 | public ActivityTestRule activityRule = new ActivityTestRule<>(HomeActivity.class, 35 | true, 36 | false); 37 | 38 | @Before 39 | public void setUp() { 40 | Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 41 | KaifApplication app = (KaifApplication) instrumentation.getTargetContext() 42 | .getApplicationContext(); 43 | TestBeans beans = DaggerTestBeans.builder().build(); 44 | app.setBeans(beans); 45 | beans.inject(this); 46 | Intents.init(); 47 | } 48 | 49 | @After 50 | public void tearDown() { 51 | Intents.release(); 52 | } 53 | 54 | @Test 55 | public void showLoginActivity_if_not_signIn() { 56 | Mockito.when(accountDaemon.hasAccount()).thenReturn(false); 57 | activityRule.launchActivity(new Intent()); 58 | intended(IntentMatchers.hasComponent(LoginActivity.class.getName())); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/view/LoginActivityTest.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.app.Instrumentation; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.support.test.InstrumentationRegistry; 7 | import android.support.test.espresso.intent.Intents; 8 | import android.support.test.rule.ActivityTestRule; 9 | import android.support.test.runner.AndroidJUnit4; 10 | 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.mockito.Mockito; 17 | 18 | import javax.inject.Inject; 19 | 20 | import io.kaif.mobile.DaggerTestBeans; 21 | import io.kaif.mobile.KaifApplication; 22 | import io.kaif.mobile.R; 23 | import io.kaif.mobile.TestBeans; 24 | import io.kaif.mobile.view.daemon.AccountDaemon; 25 | 26 | import static android.support.test.espresso.Espresso.onView; 27 | import static android.support.test.espresso.action.ViewActions.click; 28 | import static android.support.test.espresso.intent.Intents.intended; 29 | import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; 30 | import static android.support.test.espresso.intent.matcher.IntentMatchers.hasData; 31 | import static android.support.test.espresso.intent.matcher.UriMatchers.hasHost; 32 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 33 | import static org.hamcrest.Matchers.allOf; 34 | import static org.hamcrest.Matchers.equalTo; 35 | 36 | @RunWith(AndroidJUnit4.class) 37 | public class LoginActivityTest { 38 | 39 | @Inject 40 | AccountDaemon accountDaemon; 41 | 42 | @Rule 43 | public ActivityTestRule activityRule = new ActivityTestRule<>(LoginActivity.class, 44 | true, 45 | false); 46 | 47 | @Before 48 | public void setUp() { 49 | Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 50 | KaifApplication app = (KaifApplication) instrumentation.getTargetContext() 51 | .getApplicationContext(); 52 | TestBeans beans = DaggerTestBeans.builder().build(); 53 | app.setBeans(beans); 54 | beans.inject(this); 55 | Intents.init(); 56 | } 57 | 58 | @After 59 | public void tearDown() { 60 | Intents.release(); 61 | } 62 | 63 | @Test 64 | public void showOauthPage() { 65 | 66 | final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://foo.com")); 67 | 68 | Mockito.when(accountDaemon.createOauthPageIntent()).thenReturn(intent); 69 | 70 | activityRule.launchActivity(new Intent()); 71 | 72 | onView(withId(R.id.sign_in)).perform(click()); 73 | 74 | intended(allOf(hasAction(equalTo(Intent.ACTION_VIEW)), 75 | hasData(hasHost(equalTo("foo.com"))))); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/view/daemon/MockDaemonModule.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import org.mockito.Mockito; 6 | 7 | import dagger.Module; 8 | import dagger.Provides; 9 | 10 | @Module 11 | public class MockDaemonModule { 12 | 13 | @Provides 14 | @Singleton 15 | ArticleDaemon provideArticleDaemon() { 16 | return Mockito.mock(ArticleDaemon.class); 17 | } 18 | 19 | @Provides 20 | @Singleton 21 | DebateDaemon provideDebateDaemon() { 22 | return Mockito.mock(DebateDaemon.class); 23 | } 24 | 25 | @Provides 26 | @Singleton 27 | AccountDaemon provideAccountDaemon() { 28 | return Mockito.mock(AccountDaemon.class); 29 | } 30 | 31 | @Provides 32 | @Singleton 33 | ZoneDaemon provideZoneDaemon() { 34 | return Mockito.mock(ZoneDaemon.class); 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | VoteDaemon provideVoteDaemon() { 40 | return Mockito.mock(VoteDaemon.class); 41 | } 42 | 43 | @Provides 44 | @Singleton 45 | FeedDaemon provideNewsFeedDaemon() { 46 | return Mockito.mock(FeedDaemon.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/kaif/mobile/view/daemon/VoteDaemonTest.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import static org.mockito.Mockito.*; 4 | 5 | import java.io.IOException; 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | import org.mockito.Mock; 11 | import org.mockito.MockitoAnnotations; 12 | 13 | import android.test.AndroidTestCase; 14 | import io.kaif.mobile.event.vote.VoteArticleSuccessEvent; 15 | import io.kaif.mobile.event.vote.VoteDebateSuccessEvent; 16 | import io.kaif.mobile.model.Vote; 17 | import io.kaif.mobile.service.VoteService; 18 | import io.kaif.mobile.test.ModelFixture; 19 | import rx.Observable; 20 | 21 | public class VoteDaemonTest extends AndroidTestCase implements ModelFixture { 22 | @Mock 23 | private VoteService mockVoteService; 24 | 25 | private VoteDaemon daemon; 26 | 27 | @Override 28 | protected void setUp() throws Exception { 29 | super.setUp(); 30 | MockitoAnnotations.initMocks(this); 31 | daemon = new VoteDaemon(mockVoteService); 32 | } 33 | 34 | public void testVoteArticle() throws InterruptedException { 35 | final Observable expected = Observable.just(null); 36 | when(mockVoteService.voteArticle(new VoteService.VoteArticleEntry("aId", 37 | Vote.VoteState.UP))).thenReturn(expected); 38 | 39 | AtomicReference ref = new AtomicReference<>(); 40 | CountDownLatch latch = new CountDownLatch(1); 41 | daemon.getSubject(VoteArticleSuccessEvent.class) 42 | .cast(VoteArticleSuccessEvent.class) 43 | .subscribe(articleEvent -> { 44 | ref.set(articleEvent); 45 | latch.countDown(); 46 | }); 47 | 48 | daemon.voteArticle("aId", Vote.VoteState.EMPTY, Vote.VoteState.UP); 49 | latch.await(3, TimeUnit.SECONDS); 50 | assertEquals(new VoteArticleSuccessEvent("aId", Vote.VoteState.UP), ref.get()); 51 | } 52 | 53 | public void testVoteDebate() throws InterruptedException { 54 | final Observable expected = Observable.just(null); 55 | when(mockVoteService.voteDebate(new VoteService.VoteDebateEntry("aId", 56 | Vote.VoteState.UP))).thenReturn(expected); 57 | 58 | AtomicReference ref = new AtomicReference<>(); 59 | CountDownLatch latch = new CountDownLatch(1); 60 | daemon.getSubject(VoteDebateSuccessEvent.class) 61 | .cast(VoteDebateSuccessEvent.class) 62 | .subscribe(debateSuccessEvent -> { 63 | ref.set(debateSuccessEvent); 64 | latch.countDown(); 65 | }); 66 | 67 | daemon.voteDebate("aId", Vote.VoteState.EMPTY, Vote.VoteState.UP); 68 | latch.await(3, TimeUnit.SECONDS); 69 | assertEquals(new VoteDebateSuccessEvent("aId", Vote.VoteState.UP), ref.get()); 70 | } 71 | 72 | public void testVoteArticle_failed() throws InterruptedException { 73 | final Observable expected = Observable.error(new IOException()); 74 | when(mockVoteService.voteArticle(new VoteService.VoteArticleEntry("aId", 75 | Vote.VoteState.UP))).thenReturn(expected); 76 | 77 | AtomicReference ref = new AtomicReference<>(); 78 | CountDownLatch latch = new CountDownLatch(2); 79 | daemon.getSubject(VoteArticleSuccessEvent.class) 80 | .cast(VoteArticleSuccessEvent.class) 81 | .subscribe(articleEvent -> { 82 | ref.set(articleEvent); 83 | latch.countDown(); 84 | }); 85 | 86 | daemon.voteArticle("aId", Vote.VoteState.EMPTY, Vote.VoteState.UP); 87 | 88 | latch.await(3, TimeUnit.SECONDS); 89 | assertEquals(new VoteArticleSuccessEvent("aId", Vote.VoteState.EMPTY), ref.get()); 90 | } 91 | 92 | public void testVoteDebate_failed() throws InterruptedException { 93 | final Observable expected = Observable.error(new IOException()); 94 | when(mockVoteService.voteDebate(new VoteService.VoteDebateEntry("aId", 95 | Vote.VoteState.UP))).thenReturn(expected); 96 | 97 | AtomicReference ref = new AtomicReference<>(); 98 | CountDownLatch latch = new CountDownLatch(2); 99 | daemon.getSubject(VoteDebateSuccessEvent.class).subscribe(event -> { 100 | ref.set(event); 101 | latch.countDown(); 102 | }); 103 | 104 | daemon.voteDebate("aId", Vote.VoteState.EMPTY, Vote.VoteState.UP); 105 | 106 | latch.await(3, TimeUnit.SECONDS); 107 | assertEquals(new VoteDebateSuccessEvent("aId", Vote.VoteState.EMPTY), ref.get()); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 59 | 60 | 63 | 66 | 67 | 71 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/Beans.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile; 2 | 3 | import android.app.Application; 4 | 5 | import javax.inject.Singleton; 6 | 7 | import dagger.Component; 8 | import io.kaif.mobile.service.ServiceModule; 9 | import io.kaif.mobile.util.UtilModule; 10 | import io.kaif.mobile.view.ArticlesFragment; 11 | import io.kaif.mobile.view.DebatesActivity; 12 | import io.kaif.mobile.view.DebatesFragment; 13 | import io.kaif.mobile.view.HomeActivity; 14 | import io.kaif.mobile.view.HomeFragment; 15 | import io.kaif.mobile.view.HonorFragment; 16 | import io.kaif.mobile.view.LatestDebatesFragment; 17 | import io.kaif.mobile.view.LoginActivity; 18 | import io.kaif.mobile.view.NewsFeedActivity; 19 | import io.kaif.mobile.view.NewsFeedActivityFragment; 20 | import io.kaif.mobile.view.share.ShareArticleActivity; 21 | import io.kaif.mobile.view.share.ShareExternalLinkFragment; 22 | import io.kaif.mobile.view.widget.ArticleScoreTextView; 23 | import io.kaif.mobile.view.widget.DebateActions; 24 | import io.kaif.mobile.view.widget.ReplyDialog; 25 | import io.kaif.mobile.view.widget.VoteArticleButton; 26 | 27 | @Singleton 28 | @Component(modules = { ServiceModule.class, UtilModule.class }) 29 | public interface Beans { 30 | 31 | void inject(HomeActivity activity); 32 | 33 | void inject(KaifApplication kaifApplication); 34 | 35 | void inject(ShareArticleActivity activity); 36 | 37 | void inject(ShareExternalLinkFragment fragment); 38 | 39 | void inject(ArticlesFragment fragment); 40 | 41 | void inject(DebatesActivity debatesActivity); 42 | 43 | void inject(DebatesFragment debatesFragment); 44 | 45 | void inject(VoteArticleButton view); 46 | 47 | void inject(ArticleScoreTextView view); 48 | 49 | void inject(DebateActions debateActions); 50 | 51 | void inject(ReplyDialog replyDialog); 52 | 53 | void inject(HomeFragment homeFragment); 54 | 55 | void inject(LoginActivity loginActivity); 56 | 57 | void inject(LatestDebatesFragment latestDebatesFragment); 58 | 59 | void inject(NewsFeedActivity newsFeedActivity); 60 | 61 | void inject(NewsFeedActivityFragment newsFeedActivityFragment); 62 | 63 | void inject(HonorFragment honorFragment); 64 | 65 | final class Initializer { 66 | public static Beans init(Application application) { 67 | return DaggerBeans.builder() 68 | .utilModule(new UtilModule(application)) 69 | .serviceModule(new ServiceModule(application)) 70 | .build(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/IgnoreAllSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile; 2 | 3 | import rx.Subscriber; 4 | 5 | public class IgnoreAllSubscriber extends Subscriber { 6 | 7 | @Override 8 | public void onCompleted() { 9 | 10 | } 11 | 12 | @Override 13 | public void onError(Throwable e) { 14 | 15 | } 16 | 17 | @Override 18 | public void onNext(T o) { 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/KaifApplication.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile; 2 | 3 | import android.app.Application; 4 | 5 | public class KaifApplication extends Application { 6 | 7 | private static KaifApplication INSTANCE; 8 | 9 | private Beans beans; 10 | 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | INSTANCE = this; 15 | beans = Beans.Initializer.init(this); 16 | } 17 | 18 | public static KaifApplication getInstance() { 19 | return INSTANCE; 20 | } 21 | 22 | public Beans beans() { 23 | return beans; 24 | } 25 | 26 | public void setBeans(Beans beans) { 27 | this.beans = beans; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/app/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.app; 2 | 3 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity; 4 | 5 | import rx.Observable; 6 | import rx.android.schedulers.AndroidSchedulers; 7 | 8 | public class BaseActivity extends RxAppCompatActivity { 9 | 10 | protected Observable bind(Observable observable) { 11 | return observable.compose(bindToLifecycle()).observeOn(AndroidSchedulers.mainThread()); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/app/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.app; 2 | 3 | import com.trello.rxlifecycle.components.support.RxFragment; 4 | 5 | import rx.Observable; 6 | import rx.android.schedulers.AndroidSchedulers; 7 | 8 | public class BaseFragment extends RxFragment { 9 | 10 | protected Observable bind(Observable observable) { 11 | return observable.compose(bindToLifecycle()).observeOn(AndroidSchedulers.mainThread()); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/config/ApiConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.config; 2 | 3 | public class ApiConfiguration { 4 | 5 | private String endPoint; 6 | 7 | private String clientId; 8 | 9 | private String redirectUri; 10 | 11 | private String clientSecret; 12 | 13 | public ApiConfiguration(String endPoint, 14 | String clientId, 15 | String clientSecret, 16 | String redirectUri) { 17 | this.endPoint = endPoint; 18 | this.clientId = clientId; 19 | this.clientSecret = clientSecret; 20 | this.redirectUri = redirectUri; 21 | } 22 | 23 | public String getEndPoint() { 24 | return endPoint; 25 | } 26 | 27 | public String getClientId() { 28 | return clientId; 29 | } 30 | 31 | public String getRedirectUri() { 32 | return redirectUri; 33 | } 34 | 35 | public String getClientSecret() { 36 | return clientSecret; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/EventPublishSubject.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event; 2 | 3 | import rx.Observable; 4 | import rx.android.schedulers.AndroidSchedulers; 5 | import rx.subjects.PublishSubject; 6 | 7 | public class EventPublishSubject { 8 | 9 | private final PublishSubject subject; 10 | 11 | public EventPublishSubject() { 12 | this.subject = PublishSubject.create(); 13 | } 14 | 15 | public Observable getSubject(Class... classes) { 16 | return subject.asObservable().filter(event -> { 17 | for (Class clazz : classes) { 18 | if (clazz.isInstance(event)) { 19 | return true; 20 | } 21 | } 22 | return false; 23 | }).observeOn(AndroidSchedulers.mainThread()); 24 | } 25 | 26 | public Observable getSubject(Class clazz) { 27 | return subject.asObservable() 28 | .filter(clazz::isInstance) 29 | .cast(clazz) 30 | .observeOn(AndroidSchedulers.mainThread()); 31 | } 32 | 33 | public void onNext(E event) { 34 | subject.onNext(event); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/account/AccountEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.account; 2 | 3 | public class AccountEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/account/SignInSuccessEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.account; 2 | 3 | public class SignInSuccessEvent extends AccountEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/account/SignOutEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.account; 2 | 3 | public class SignOutEvent extends AccountEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/article/ArticleEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.article; 2 | 3 | public class ArticleEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/debate/CreateDebateFailedEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.debate; 2 | 3 | import io.kaif.mobile.event.article.ArticleEvent; 4 | 5 | public class CreateDebateFailedEvent extends DebateEvent { 6 | private String localId; 7 | 8 | public CreateDebateFailedEvent(String localId) { 9 | 10 | this.localId = localId; 11 | } 12 | 13 | public String getLocalId() { 14 | return localId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/debate/CreateDebateSuccessEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.debate; 2 | 3 | import io.kaif.mobile.event.article.ArticleEvent; 4 | import io.kaif.mobile.model.Debate; 5 | 6 | public class CreateDebateSuccessEvent extends DebateEvent { 7 | 8 | private String localId; 9 | 10 | private Debate debate; 11 | 12 | public CreateDebateSuccessEvent(String localId, Debate debate) { 13 | this.localId = localId; 14 | this.debate = debate; 15 | } 16 | 17 | public String getLocalId() { 18 | return localId; 19 | } 20 | 21 | public Debate getDebate() { 22 | return debate; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/debate/CreateLocalDebateEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.debate; 2 | 3 | import io.kaif.mobile.model.LocalDebate; 4 | 5 | public class CreateLocalDebateEvent extends DebateEvent { 6 | private LocalDebate localDebate; 7 | 8 | public CreateLocalDebateEvent(String articleId, 9 | String localDebateId, 10 | String parentDebateId, 11 | int level, 12 | String content) { 13 | this.localDebate = new LocalDebate(articleId, localDebateId, parentDebateId, level, content); 14 | } 15 | 16 | public LocalDebate getLocalDebate() { 17 | return localDebate; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/debate/DebateEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.debate; 2 | 3 | public class DebateEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/vote/VoteArticleSuccessEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.vote; 2 | 3 | import io.kaif.mobile.model.Vote; 4 | 5 | public class VoteArticleSuccessEvent extends VoteEvent { 6 | private final String articleId; 7 | private final Vote.VoteState voteState; 8 | 9 | public VoteArticleSuccessEvent(String articleId, Vote.VoteState voteState) { 10 | this.articleId = articleId; 11 | this.voteState = voteState; 12 | } 13 | 14 | public String getArticleId() { 15 | return articleId; 16 | } 17 | 18 | public Vote.VoteState getVoteState() { 19 | return voteState; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "VoteArticleSuccessEvent{" + 25 | "articleId='" + articleId + '\'' + 26 | ", voteState=" + voteState + 27 | '}'; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) { 33 | return true; 34 | } 35 | if (o == null || getClass() != o.getClass()) { 36 | return false; 37 | } 38 | 39 | VoteArticleSuccessEvent that = (VoteArticleSuccessEvent) o; 40 | 41 | if (!articleId.equals(that.articleId)) { 42 | return false; 43 | } 44 | return voteState == that.voteState; 45 | 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | int result = articleId.hashCode(); 51 | result = 31 * result + voteState.hashCode(); 52 | return result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/vote/VoteDebateSuccessEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.vote; 2 | 3 | import io.kaif.mobile.model.Vote; 4 | 5 | public class VoteDebateSuccessEvent extends VoteEvent { 6 | private final String debateId; 7 | private final Vote.VoteState voteState; 8 | 9 | public VoteDebateSuccessEvent(String debateId, Vote.VoteState voteState) { 10 | this.debateId = debateId; 11 | this.voteState = voteState; 12 | } 13 | 14 | public String getDebateId() { 15 | return debateId; 16 | } 17 | 18 | public Vote.VoteState getVoteState() { 19 | return voteState; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "VoteArticleSuccessEvent{" + 25 | "debateId='" + debateId + '\'' + 26 | ", voteState=" + voteState + 27 | '}'; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) { 33 | return true; 34 | } 35 | if (o == null || getClass() != o.getClass()) { 36 | return false; 37 | } 38 | 39 | VoteDebateSuccessEvent that = (VoteDebateSuccessEvent) o; 40 | 41 | if (!debateId.equals(that.debateId)) { 42 | return false; 43 | } 44 | return voteState == that.voteState; 45 | 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | int result = debateId.hashCode(); 51 | result = 31 * result + voteState.hashCode(); 52 | return result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/event/vote/VoteEvent.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.event.vote; 2 | 3 | public class VoteEvent { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/json/ApiResponseDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.json; 2 | 3 | import java.lang.reflect.Type; 4 | 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonDeserializationContext; 7 | import com.google.gson.JsonDeserializer; 8 | import com.google.gson.JsonElement; 9 | import com.google.gson.JsonParseException; 10 | 11 | public class ApiResponseDeserializer implements JsonDeserializer { 12 | 13 | private Gson gson; 14 | 15 | public ApiResponseDeserializer(Gson gson) { 16 | this.gson = gson; 17 | } 18 | 19 | @Override 20 | public Object deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) 21 | throws JsonParseException { 22 | JsonElement content = je.getAsJsonObject().get("data"); 23 | return gson.fromJson(content, type); 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/BlockType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * Block type enum. 21 | * 22 | * @author René Jeschke 23 | */ 24 | enum BlockType { 25 | /** 26 | * Unspecified. Used for root block and list items without paragraphs. 27 | */ 28 | NONE, 29 | /** 30 | * A fenced code block. 31 | */ 32 | FENCED_CODE, 33 | /** 34 | * A ordered list item. 35 | */ 36 | ORDERED_LIST_ITEM, 37 | /** 38 | * A unordered list item. 39 | */ 40 | UNORDERED_LIST_ITEM, 41 | /** 42 | * An ordered list. 43 | */ 44 | ORDERED_LIST, 45 | /** 46 | * A paragraph. 47 | */ 48 | PARAGRAPH, 49 | /** 50 | * An unordered list. 51 | */ 52 | UNORDERED_LIST, 53 | 54 | /** 55 | * Block Quote 56 | */ 57 | BLOCKQUOTE, 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/Configuration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * Txtmark configuration. 21 | * 22 | * @author René Jeschke 23 | * @since 0.7 24 | */ 25 | public class Configuration { 26 | 27 | /** 28 | * Configuration builder. 29 | * 30 | * @author René Jeschke 31 | * @since 0.7 32 | */ 33 | public static class Builder { 34 | 35 | private String encoding = "UTF-8"; 36 | private Decorator decorator; 37 | 38 | /** 39 | * Constructor. 40 | */ 41 | Builder() { 42 | // empty 43 | } 44 | 45 | /** 46 | * Sets the character encoding for txtmark. 47 | *

48 | * Default: "UTF-8" 49 | * 50 | * @param encoding 51 | * The encoding 52 | * @return This builder 53 | * @since 0.7 54 | */ 55 | public Builder setEncoding(String encoding) { 56 | this.encoding = encoding; 57 | return this; 58 | } 59 | 60 | /** 61 | * Builds a configuration instance. 62 | * 63 | * @return a Configuration instance 64 | * @since 0.7 65 | */ 66 | public Configuration build() { 67 | return new Configuration(this.encoding, this.decorator); 68 | } 69 | 70 | public Decorator getDecorator() { 71 | return decorator; 72 | } 73 | 74 | /** 75 | * Sets the decorator for txtmark. 76 | *

77 | * Default: DefaultDecorator() 78 | * 79 | * @param decorator 80 | * The decorator 81 | * @return This builder 82 | * @see DefaultDecorator 83 | * @since 0.7 84 | */ 85 | public Builder setDecorator(Decorator decorator) { 86 | this.decorator = decorator; 87 | return this; 88 | } 89 | } 90 | 91 | /** 92 | * Creates a new Builder instance. 93 | * 94 | * @return A new Builder instance. 95 | */ 96 | public static Builder builder() { 97 | return new Builder(); 98 | } 99 | 100 | final String encoding; 101 | final Decorator decorator; 102 | 103 | Configuration(String encoding, Decorator decorator) { 104 | this.encoding = encoding; 105 | this.decorator = decorator; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/LineType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * Line type enumeration. 21 | * 22 | * @author René Jeschke 23 | */ 24 | enum LineType { 25 | /** 26 | * Empty line. 27 | */ 28 | EMPTY, 29 | /** 30 | * Undefined content. 31 | */ 32 | OTHER, 33 | /** 34 | * A list. 35 | */ 36 | ULIST, OLIST, 37 | /** 38 | * Fenced code block start/end 39 | */ 40 | FENCED_CODE, 41 | /** 42 | * A block quote. 43 | */ 44 | BQUOTE 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/LinkRef.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * A markdown link reference. 21 | * 22 | * @author René Jeschke 23 | */ 24 | class LinkRef { 25 | 26 | /** 27 | * reference sequence 28 | */ 29 | public final int seqNumber; 30 | 31 | /** 32 | * The link. 33 | */ 34 | public final String link; 35 | /** 36 | * The optional comment/title. 37 | */ 38 | public String title; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param link 44 | * The link. 45 | * @param title 46 | * The title (may be null). 47 | */ 48 | public LinkRef(final int seqNumber, final String link, final String title) { 49 | this.seqNumber = seqNumber; 50 | this.link = link; 51 | this.title = title; 52 | } 53 | 54 | /** 55 | * @see Object#toString() 56 | */ 57 | @Override 58 | public String toString() { 59 | return this.link + " \"" + this.title + "\""; 60 | } 61 | 62 | public boolean hasHttpScheme() { 63 | return link.startsWith("http://") || link.startsWith("https://"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/MarkToken.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * Markdown token enumeration. 21 | * 22 | * @author René Jeschke 23 | */ 24 | enum MarkToken { 25 | /** 26 | * No token. 27 | */ 28 | NONE, 29 | /** 30 | * * 31 | */ 32 | EM_STAR, // x*x 33 | /** 34 | * _ 35 | */ 36 | EM_UNDERSCORE, // x_x 37 | /** 38 | * ** 39 | */ 40 | STRONG_STAR, // x**x 41 | /** 42 | * __ 43 | */ 44 | STRONG_UNDERSCORE, // x__x 45 | /** 46 | * ~~ 47 | */ 48 | STRIKE, // x~~x 49 | /** 50 | * ` 51 | */ 52 | CODE_SINGLE, // ` 53 | /** 54 | * `` 55 | */ 56 | CODE_DOUBLE, // `` 57 | /** 58 | * [ 59 | */ 60 | LINK, // [ 61 | /** 62 | * \ 63 | */ 64 | ESCAPE, // \x 65 | /** 66 | * Extended: ^ 67 | */ 68 | SUPER, // ^ 69 | /** 70 | * Extended: /u/NAME_PATTERN 71 | */ 72 | USER, 73 | /** 74 | * Extended: /z/ZONE_PATTERN 75 | */ 76 | ZONE, 77 | 78 | BR, 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Koji Lin 3 | * Copyright (C) 2011 René Jeschke 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package io.kaif.mobile.kmark; 18 | 19 | /** 20 | * Utilities. 21 | * 22 | * @author René Jeschke 23 | */ 24 | class Utils { 25 | 26 | /** 27 | * Skips spaces in the given String. 28 | * 29 | * @param in 30 | * Input String. 31 | * @param start 32 | * Starting position. 33 | * @return The new position or -1 if EOL has been reached. 34 | */ 35 | public final static int skipSpaces(final String in, final int start) { 36 | int pos = start; 37 | while (pos < in.length() && (in.charAt(pos) == ' ' || in.charAt(pos) == '\n')) { 38 | pos++; 39 | } 40 | return pos < in.length() ? pos : -1; 41 | } 42 | 43 | /** 44 | * Reads a markdown link ID. 45 | * 46 | * @param out 47 | * The StringBuilder to write to. 48 | * @param in 49 | * Input String. 50 | * @param start 51 | * Starting position. 52 | * @return The new position or -1 if this is no valid markdown link ID. 53 | */ 54 | public final static int readMdLinkId(final StringBuilder out, final String in, final int start) { 55 | int pos = start; 56 | int counter = 1; 57 | while (pos < in.length()) { 58 | final char ch = in.charAt(pos); 59 | boolean endReached = false; 60 | switch (ch) { 61 | case '\n': 62 | out.append(' '); 63 | break; 64 | case '[': 65 | counter++; 66 | out.append(ch); 67 | break; 68 | case ']': 69 | counter--; 70 | if (counter == 0) { 71 | endReached = true; 72 | } else { 73 | out.append(ch); 74 | } 75 | break; 76 | default: 77 | out.append(ch); 78 | break; 79 | } 80 | if (endReached) { 81 | break; 82 | } 83 | pos++; 84 | } 85 | 86 | return (pos == in.length()) ? -1 : pos; 87 | } 88 | 89 | /** 90 | * Reads characters until the end character is encountered, ignoring escape 91 | * sequences. 92 | * 93 | * @param out 94 | * The StringBuilder to write to. 95 | * @param in 96 | * The Input String. 97 | * @param start 98 | * Starting position. 99 | * @param end 100 | * End characters. 101 | * @return The new position or -1 if no 'end' char was found. 102 | */ 103 | public final static int readRawUntil(final StringBuilder out, 104 | final String in, 105 | final int start, 106 | final char end) { 107 | int pos = start; 108 | while (pos < in.length()) { 109 | final char ch = in.charAt(pos); 110 | if (ch == end) { 111 | break; 112 | } 113 | out.append(ch); 114 | pos++; 115 | } 116 | 117 | return (pos == in.length()) ? -1 : pos; 118 | } 119 | 120 | /** 121 | * Removes trailing ` and trims spaces. 122 | * 123 | * @param fenceLine 124 | * Fenced code block starting line 125 | * @return Rest of the line after trimming and backtick removal 126 | * @since 0.7 127 | */ 128 | public final static String getMetaFromFence(String fenceLine) { 129 | for (int i = 0; i < fenceLine.length(); i++) { 130 | final char c = fenceLine.charAt(i); 131 | if (!Character.isWhitespace(c) && c != '`' && c != '~' && c != '%') { 132 | return fenceLine.substring(i).trim(); 133 | } 134 | } 135 | return ""; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/text/BulletSpan2.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.kmark.text; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Path; 6 | import android.text.Layout; 7 | import android.text.Spanned; 8 | import android.text.style.LeadingMarginSpan; 9 | 10 | public class BulletSpan2 implements LeadingMarginSpan { 11 | private int leading; 12 | private final int gapWidth; 13 | private final int bulletRadius; 14 | 15 | private static Path sBulletPath = null; 16 | 17 | public BulletSpan2(int leading, int gapWidth, int bulletRadius) { 18 | this.leading = leading; 19 | this.gapWidth = gapWidth; 20 | this.bulletRadius = bulletRadius; 21 | } 22 | 23 | public int getLeadingMargin(boolean first) { 24 | return leading + (2 * bulletRadius + gapWidth); 25 | } 26 | 27 | public void drawLeadingMargin(Canvas c, 28 | Paint p, 29 | int x, 30 | int dir, 31 | int top, 32 | int baseline, 33 | int bottom, 34 | CharSequence text, 35 | int start, 36 | int end, 37 | boolean first, 38 | Layout l) { 39 | if (((Spanned) text).getSpanStart(this) == start) { 40 | Paint.Style style = p.getStyle(); 41 | p.setStyle(Paint.Style.FILL); 42 | 43 | if (c.isHardwareAccelerated()) { 44 | if (sBulletPath == null) { 45 | sBulletPath = new Path(); 46 | // Bullet is slightly better to avoid aliasing artifacts on mdpi devices. 47 | sBulletPath.addCircle(0.0f, 0.0f, 1.2f * bulletRadius, Path.Direction.CW); 48 | } 49 | 50 | c.save(); 51 | c.translate(x + dir * bulletRadius + leading, (top + bottom) / 2.0f); 52 | c.drawPath(sBulletPath, p); 53 | c.restore(); 54 | } else { 55 | c.drawCircle(x + dir * bulletRadius + leading, (top + bottom) / 2.0f, bulletRadius, p); 56 | } 57 | 58 | p.setStyle(style); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/text/CodeBlockSpan.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.kmark.text; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.text.style.LineBackgroundSpan; 6 | 7 | public class CodeBlockSpan implements LineBackgroundSpan { 8 | private int color; 9 | 10 | public CodeBlockSpan(int color) { 11 | this.color = color; 12 | } 13 | 14 | @Override 15 | public void drawBackground(Canvas c, 16 | Paint p, 17 | int left, 18 | int right, 19 | int top, 20 | int baseline, 21 | int bottom, 22 | CharSequence text, 23 | int start, 24 | int end, 25 | int lnum) { 26 | int oldcolor = p.getColor(); 27 | p.setColor(color); 28 | c.drawRect(left, top, right, bottom, p); 29 | p.setColor(oldcolor); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/kmark/text/SuperscriptSpan2.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.kmark.text; 2 | 3 | import android.support.annotation.NonNull; 4 | import android.text.TextPaint; 5 | import android.text.style.SuperscriptSpan; 6 | 7 | public class SuperscriptSpan2 extends SuperscriptSpan { 8 | @Override 9 | public void updateDrawState(@NonNull TextPaint tp) { 10 | tp.setTextSize(tp.getTextSize() * 0.75f); 11 | tp.baselineShift += (int) (tp.ascent() / 2); 12 | } 13 | 14 | @Override 15 | public void updateMeasureState(@NonNull TextPaint tp) { 16 | tp.setTextSize(tp.getTextSize() * 0.75f); 17 | tp.baselineShift += (int) (tp.ascent() / 2); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/Article.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | public class Article implements Serializable { 7 | 8 | public enum ArticleType { 9 | EXTERNAL_LINK, SPEAK 10 | } 11 | 12 | private final String zone; 13 | 14 | private final String zoneTitle; 15 | 16 | private final String articleId; 17 | 18 | private final String title; 19 | 20 | private final Date createTime; 21 | 22 | private final String link; 23 | 24 | private final String content; 25 | 26 | private final ArticleType articleType; 27 | 28 | private final String authorName; 29 | 30 | private final long upVote; 31 | 32 | private final long debateCount; 33 | 34 | public Article(String zone, 35 | String zoneTitle, 36 | String articleId, 37 | String title, 38 | Date createTime, 39 | String link, 40 | String content, 41 | ArticleType articleType, 42 | String authorName, 43 | long upVote, 44 | long debateCount) { 45 | this.zone = zone; 46 | this.zoneTitle = zoneTitle; 47 | this.articleId = articleId; 48 | this.title = title; 49 | this.createTime = createTime; 50 | this.link = link; 51 | this.content = content; 52 | this.articleType = articleType; 53 | this.authorName = authorName; 54 | this.upVote = upVote; 55 | this.debateCount = debateCount; 56 | } 57 | 58 | public String getZone() { 59 | return zone; 60 | } 61 | 62 | public String getZoneTitle() { 63 | return zoneTitle; 64 | } 65 | 66 | public String getArticleId() { 67 | return articleId; 68 | } 69 | 70 | public String getTitle() { 71 | return title; 72 | } 73 | 74 | public Date getCreateTime() { 75 | return createTime; 76 | } 77 | 78 | public String getLink() { 79 | return link; 80 | } 81 | 82 | public String getContent() { 83 | return content; 84 | } 85 | 86 | public ArticleType getArticleType() { 87 | return articleType; 88 | } 89 | 90 | public String getAuthorName() { 91 | return authorName; 92 | } 93 | 94 | public long getUpVote() { 95 | return upVote; 96 | } 97 | 98 | public long getDebateCount() { 99 | return debateCount; 100 | } 101 | 102 | @Override 103 | public boolean equals(Object o) { 104 | if (this == o) { 105 | return true; 106 | } 107 | if (o == null || getClass() != o.getClass()) { 108 | return false; 109 | } 110 | 111 | Article article = (Article) o; 112 | 113 | return articleId.equals(article.articleId); 114 | 115 | } 116 | 117 | @Override 118 | public int hashCode() { 119 | return articleId.hashCode(); 120 | } 121 | 122 | @Override 123 | public String toString() { 124 | return "Article{" + 125 | "zone='" + zone + '\'' + 126 | ", zoneTitle='" + zoneTitle + '\'' + 127 | ", articleId='" + articleId + '\'' + 128 | ", title='" + title + '\'' + 129 | ", createTime=" + createTime + 130 | ", link='" + link + '\'' + 131 | ", content='" + content + '\'' + 132 | ", articleType=" + articleType + 133 | ", authorName='" + authorName + '\'' + 134 | ", upVote=" + upVote + 135 | ", debateCount=" + debateCount + 136 | '}'; 137 | } 138 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/Debate.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | import java.util.Date; 4 | 5 | public class Debate { 6 | private final String articleId; 7 | 8 | private final String debateId; 9 | 10 | private final String zone; 11 | 12 | private final String parentDebateId; 13 | 14 | private final int level; 15 | 16 | private final String content; 17 | 18 | private final String debaterName; 19 | 20 | private final long upVote; 21 | 22 | private final long downVote; 23 | 24 | private final Date createTime; 25 | 26 | private final Date lastUpdateTime; 27 | 28 | public Debate(String articleId, 29 | String debateId, 30 | String zone, 31 | String parentDebateId, 32 | int level, 33 | String content, 34 | String debaterName, 35 | long upVote, 36 | long downVote, 37 | Date createTime, 38 | Date lastUpdateTime) { 39 | this.articleId = articleId; 40 | this.debateId = debateId; 41 | this.zone = zone; 42 | this.parentDebateId = parentDebateId; 43 | this.level = level; 44 | this.content = content; 45 | this.debaterName = debaterName; 46 | this.upVote = upVote; 47 | this.downVote = downVote; 48 | this.createTime = createTime; 49 | this.lastUpdateTime = lastUpdateTime; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "Debate{" + 55 | "articleId='" + articleId + '\'' + 56 | ", debateId='" + debateId + '\'' + 57 | ", zone='" + zone + '\'' + 58 | ", parentDebateId='" + parentDebateId + '\'' + 59 | ", level=" + level + 60 | ", content='" + content + '\'' + 61 | ", debaterName='" + debaterName + '\'' + 62 | ", upVote=" + upVote + 63 | ", downVote=" + downVote + 64 | ", createTime=" + createTime + 65 | ", lastUpdateTime=" + lastUpdateTime + 66 | '}'; 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) { 72 | return true; 73 | } 74 | if (o == null || getClass() != o.getClass()) { 75 | return false; 76 | } 77 | 78 | Debate debate = (Debate) o; 79 | 80 | return debateId.equals(debate.debateId); 81 | 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | return debateId.hashCode(); 87 | } 88 | 89 | public String getArticleId() { 90 | return articleId; 91 | } 92 | 93 | public String getDebateId() { 94 | return debateId; 95 | } 96 | 97 | public String getZone() { 98 | return zone; 99 | } 100 | 101 | public String getParentDebateId() { 102 | return parentDebateId; 103 | } 104 | 105 | public int getLevel() { 106 | return level; 107 | } 108 | 109 | public String getContent() { 110 | return content; 111 | } 112 | 113 | public String getDebaterName() { 114 | return debaterName; 115 | } 116 | 117 | public long getUpVote() { 118 | return upVote; 119 | } 120 | 121 | public long getDownVote() { 122 | return downVote; 123 | } 124 | 125 | public Date getCreateTime() { 126 | return createTime; 127 | } 128 | 129 | public Date getLastUpdateTime() { 130 | return lastUpdateTime; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/DebateNode.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | import java.util.List; 4 | 5 | public class DebateNode { 6 | 7 | private final Debate debate; 8 | 9 | private final List children; 10 | 11 | public DebateNode(Debate debate, List children) { 12 | this.debate = debate; 13 | this.children = children; 14 | } 15 | 16 | public Debate getDebate() { 17 | return debate; 18 | } 19 | 20 | public List getChildren() { 21 | return children; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) { 27 | return true; 28 | } 29 | if (o == null || getClass() != o.getClass()) { 30 | return false; 31 | } 32 | 33 | DebateNode that = (DebateNode) o; 34 | 35 | if (debate != null ? !debate.equals(that.debate) : that.debate != null) { 36 | return false; 37 | } 38 | return children.equals(that.children); 39 | 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | int result = debate != null ? debate.hashCode() : 0; 45 | result = 31 * result + children.hashCode(); 46 | return result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/FeedAsset.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | import android.support.annotation.Nullable; 7 | 8 | public class FeedAsset implements Serializable { 9 | enum AssetType { 10 | DEBATE_FROM_REPLY 11 | } 12 | 13 | private String assetId; 14 | 15 | private AssetType assetType; 16 | 17 | private Date createTime; 18 | 19 | private boolean acknowledged; 20 | 21 | @Nullable 22 | private Debate debate; 23 | 24 | public FeedAsset(String assetId, 25 | AssetType assetType, 26 | Date createTime, 27 | boolean acknowledged, 28 | @Nullable Debate debate) { 29 | this.assetId = assetId; 30 | this.assetType = assetType; 31 | this.createTime = createTime; 32 | this.acknowledged = acknowledged; 33 | this.debate = debate; 34 | } 35 | 36 | public String getAssetId() { 37 | return assetId; 38 | } 39 | 40 | public AssetType getAssetType() { 41 | return assetType; 42 | } 43 | 44 | public Date getCreateTime() { 45 | return createTime; 46 | } 47 | 48 | public boolean isAcknowledged() { 49 | return acknowledged; 50 | } 51 | 52 | @Nullable 53 | public Debate getDebate() { 54 | return debate; 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if (this == o) { 60 | return true; 61 | } 62 | if (o == null || getClass() != o.getClass()) { 63 | return false; 64 | } 65 | 66 | FeedAsset feedAsset = (FeedAsset) o; 67 | 68 | return assetId.equals(feedAsset.assetId); 69 | 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | return assetId.hashCode(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/LocalDebate.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | public class LocalDebate { 4 | 5 | private final String articleId; 6 | 7 | private final String localDebateId; 8 | 9 | private final String parentDebateId; 10 | 11 | private final int level; 12 | 13 | private final String content; 14 | 15 | public LocalDebate(String articleId, 16 | String localDebateId, 17 | String parentDebateId, 18 | int level, 19 | String content) { 20 | this.articleId = articleId; 21 | this.localDebateId = localDebateId; 22 | this.parentDebateId = parentDebateId; 23 | this.level = level; 24 | this.content = content; 25 | } 26 | 27 | public String getArticleId() { 28 | return articleId; 29 | } 30 | 31 | public String getLocalDebateId() { 32 | return localDebateId; 33 | } 34 | 35 | public String getParentDebateId() { 36 | return parentDebateId; 37 | } 38 | 39 | public int getLevel() { 40 | return level; 41 | } 42 | 43 | public String getContent() { 44 | return content; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) { 50 | return true; 51 | } 52 | if (o == null || getClass() != o.getClass()) { 53 | return false; 54 | } 55 | 56 | LocalDebate that = (LocalDebate) o; 57 | 58 | return localDebateId.equals(that.localDebateId); 59 | 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | return localDebateId.hashCode(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/Vote.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | public class Vote implements Serializable { 7 | public enum VoteState { 8 | UP(1), DOWN(-1), EMPTY(0); 9 | 10 | private int score; 11 | 12 | VoteState(int score) { 13 | this.score = score; 14 | } 15 | 16 | public int delta(VoteState prevVoteState) { 17 | return score - prevVoteState.score; 18 | } 19 | } 20 | 21 | private final String targetId; 22 | private final VoteState voteState; 23 | private final Date updateTime; 24 | 25 | public Vote(String targetId, VoteState voteState, Date updateTime) { 26 | this.targetId = targetId; 27 | this.voteState = voteState; 28 | this.updateTime = updateTime; 29 | } 30 | 31 | public boolean matches(String targetId) { 32 | return this.targetId.equals(targetId); 33 | } 34 | 35 | public VoteState getVoteState() { 36 | return voteState; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) { 42 | return true; 43 | } 44 | if (o == null || getClass() != o.getClass()) { 45 | return false; 46 | } 47 | 48 | Vote vote = (Vote) o; 49 | 50 | if (!targetId.equals(vote.targetId)) { 51 | return false; 52 | } 53 | if (voteState != vote.voteState) { 54 | return false; 55 | } 56 | return updateTime.equals(vote.updateTime); 57 | 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | int result = targetId.hashCode(); 63 | result = 31 * result + voteState.hashCode(); 64 | result = 31 * result + updateTime.hashCode(); 65 | return result; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return "Vote{" + 71 | "targetId='" + targetId + '\'' + 72 | ", voteState=" + voteState + 73 | ", updateTime=" + updateTime + 74 | '}'; 75 | } 76 | 77 | public static Vote abstain(String id) { 78 | return new Vote(id, VoteState.EMPTY, new Date(0)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/Zone.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model; 2 | 3 | public class Zone { 4 | private String name; 5 | private String aliasName; 6 | 7 | public Zone(String name, String aliasName) { 8 | this.name = name; 9 | this.aliasName = aliasName; 10 | } 11 | 12 | public String getName() { 13 | return name; 14 | } 15 | 16 | public String getAliasName() { 17 | return aliasName; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) { 23 | return true; 24 | } 25 | if (o == null || getClass() != o.getClass()) { 26 | return false; 27 | } 28 | 29 | Zone zone = (Zone) o; 30 | 31 | return name.equals(zone.name); 32 | 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | return name.hashCode(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/exception/DomainException.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model.exception; 2 | 3 | public abstract class DomainException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/exception/DuplicateArticleUrlException.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model.exception; 2 | 3 | public class DuplicateArticleUrlException extends DomainException{ 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/oauth/AccessTokenInfo.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model.oauth; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class AccessTokenInfo { 6 | 7 | @SerializedName("token_type") 8 | private final String tokenType; 9 | 10 | @SerializedName("access_token") 11 | private final String accessToken; 12 | 13 | private final String scope; 14 | 15 | public AccessTokenInfo(String tokenType, String accessToken, String scope) { 16 | this.tokenType = tokenType; 17 | this.accessToken = accessToken; 18 | this.scope = scope; 19 | } 20 | 21 | public String getAuthorization() { 22 | return tokenType + " " + accessToken; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return "AccessTokenInfo{" + 28 | "tokenType='" + tokenType + '\'' + 29 | ", accessToken='" + accessToken + '\'' + 30 | ", scope='" + scope + '\'' + 31 | '}'; 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/model/oauth/AccessTokenManager.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.model.oauth; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Singleton; 5 | 6 | import com.google.gson.Gson; 7 | 8 | import android.content.SharedPreferences; 9 | 10 | @Singleton 11 | public class AccessTokenManager { 12 | 13 | public static final String ACCESS_TOKEN_KEY = "ACCESS_TOKEN"; 14 | 15 | SharedPreferences preference; 16 | 17 | Gson gson; 18 | 19 | @Inject 20 | public AccessTokenManager(SharedPreferences preference, Gson gson) { 21 | this.gson = gson; 22 | this.preference = preference; 23 | } 24 | 25 | public boolean hasAccount() { 26 | return preference.contains(ACCESS_TOKEN_KEY); 27 | } 28 | 29 | public void signOut() { 30 | preference.edit().remove(ACCESS_TOKEN_KEY).apply(); 31 | } 32 | 33 | public void saveAccount(AccessTokenInfo accessTokenInfo) { 34 | preference.edit().putString(ACCESS_TOKEN_KEY, gson.toJson(accessTokenInfo)).apply(); 35 | } 36 | 37 | public AccessTokenInfo findAccount() { 38 | return gson.fromJson(preference.getString(ACCESS_TOKEN_KEY, null), AccessTokenInfo.class); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/retrofit/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.retrofit; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | 6 | public class MethodInfo { 7 | 8 | private Method method; 9 | 10 | private boolean isObservable; 11 | 12 | private boolean isGetMethod; 13 | 14 | public MethodInfo(Method method, boolean isObservable, boolean isGetMethod) { 15 | this.method = method; 16 | this.isObservable = isObservable; 17 | this.isGetMethod = isGetMethod; 18 | } 19 | 20 | public Object invoke(Object receiver, Object[] args) 21 | throws InvocationTargetException, IllegalAccessException { 22 | return method.invoke(receiver, args); 23 | } 24 | 25 | public boolean canRetry() { 26 | return isObservable && isGetMethod; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/retrofit/RetrofitRetryStaleProxy.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.retrofit; 2 | 3 | import java.lang.reflect.Proxy; 4 | 5 | import javax.inject.Inject; 6 | 7 | import retrofit2.Retrofit; 8 | 9 | /** 10 | * TODO 11 | * Generate this using annotation processor 12 | */ 13 | public class RetrofitRetryStaleProxy { 14 | 15 | public static class RetrofitHolder { 16 | 17 | @Inject 18 | Retrofit retrofit; 19 | 20 | public RetrofitHolder(Retrofit retrofit) { 21 | this.retrofit = retrofit; 22 | } 23 | 24 | public T create(Class serviceClass) { 25 | return retrofit.create(serviceClass); 26 | } 27 | } 28 | 29 | @Inject 30 | RetrofitHolder retrofitHolder; 31 | 32 | public RetrofitRetryStaleProxy(RetrofitHolder retrofitHolder) { 33 | this.retrofitHolder = retrofitHolder; 34 | } 35 | 36 | public T create(Class serviceClass) { 37 | try { 38 | return serviceClass.cast(Proxy.newProxyInstance(serviceClass.getClassLoader(), 39 | new Class[]{serviceClass}, 40 | new RetryStaleHandler(retrofitHolder.create(Class.forName(serviceClass.getName() 41 | + "$$RetryStale"))))); 42 | } catch (ClassNotFoundException e) { 43 | return retrofitHolder.create(serviceClass); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/retrofit/RetryStaleHandler.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.retrofit; 2 | 3 | import java.lang.reflect.InvocationHandler; 4 | import java.lang.reflect.Method; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | import retrofit2.adapter.rxjava.HttpException; 8 | import retrofit2.http.GET; 9 | import rx.Observable; 10 | import rx.functions.Func1; 11 | 12 | /** 13 | * TODO 14 | * Generate this using annotation processor 15 | */ 16 | class RetryStaleHandler implements InvocationHandler { 17 | 18 | private ConcurrentHashMap methodCache; 19 | 20 | private ConcurrentHashMap retryMethodCache; 21 | 22 | private Object target; 23 | 24 | public RetryStaleHandler(Object target) { 25 | this.target = target; 26 | this.methodCache = new ConcurrentHashMap<>(); 27 | this.retryMethodCache = new ConcurrentHashMap<>(); 28 | } 29 | 30 | @Override 31 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 32 | MethodInfo methodInfo = getTargetMethodInfo(method); 33 | if (methodInfo.canRetry()) { 34 | Observable result = (Observable) methodInfo.invoke(target, args); 35 | return result.onErrorResumeNext((Func1) throwable -> { 36 | if (throwable instanceof HttpException) { 37 | try { 38 | return (Observable) getTargetCacheMethod(method).invoke(target, args); 39 | } catch (Exception e) { 40 | return Observable.error(throwable); 41 | } 42 | } 43 | return Observable.error(throwable); 44 | }); 45 | } 46 | 47 | return methodInfo.invoke(target, args); 48 | } 49 | 50 | private MethodInfo getTargetMethodInfo(Method method) throws NoSuchMethodException { 51 | MethodInfo methodInfo = methodCache.get(method); 52 | if (methodInfo == null) { 53 | Method targetMethod = target.getClass() 54 | .getMethod(method.getName(), method.getParameterTypes()); 55 | methodInfo = new MethodInfo(targetMethod, 56 | method.getReturnType().isAssignableFrom(Observable.class), 57 | method.isAnnotationPresent(GET.class)); 58 | 59 | methodCache.putIfAbsent(method, methodInfo); 60 | } 61 | return methodInfo; 62 | } 63 | 64 | private Method getTargetCacheMethod(Method method) throws NoSuchMethodException { 65 | Method targetMethod = retryMethodCache.get(method); 66 | if (targetMethod == null) { 67 | targetMethod = target.getClass() 68 | .getMethod(method.getName() + "$$RetryStale", method.getParameterTypes()); 69 | retryMethodCache.putIfAbsent(method, targetMethod); 70 | } 71 | return targetMethod; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | public interface AccountService { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/ArticleService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.List; 4 | 5 | import io.kaif.mobile.model.Article; 6 | import retrofit2.http.Body; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.PUT; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Query; 11 | import rx.Observable; 12 | 13 | public interface ArticleService { 14 | 15 | class ExternalLinkEntry { 16 | String url; 17 | String title; 18 | String zone; 19 | 20 | public ExternalLinkEntry(String url, String title, String zone) { 21 | this.url = url; 22 | this.title = title; 23 | this.zone = zone; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) { 29 | return true; 30 | } 31 | if (o == null || getClass() != o.getClass()) { 32 | return false; 33 | } 34 | 35 | ExternalLinkEntry that = (ExternalLinkEntry) o; 36 | 37 | if (!url.equals(that.url)) { 38 | return false; 39 | } 40 | if (!title.equals(that.title)) { 41 | return false; 42 | } 43 | return zone.equals(that.zone); 44 | 45 | } 46 | 47 | @Override 48 | public int hashCode() { 49 | int result = url.hashCode(); 50 | result = 31 * result + title.hashCode(); 51 | result = 31 * result + zone.hashCode(); 52 | return result; 53 | } 54 | } 55 | 56 | @PUT("/v1/article/external-link") 57 | Observable

createExternalLink(@Body ExternalLinkEntry externalLinkEntry); 58 | 59 | @GET("/v1/article/hot") 60 | Observable> listHotArticles(@Query("start-article-id") String startArticleId); 61 | 62 | @GET("/v1/article/zone/{zone}/external-link/exist") 63 | Observable exist(@Path("zone") String zone, @Query("url") String url); 64 | 65 | @GET("/v1/article/latest") 66 | Observable> listLatestArticles(@Query("start-article-id") String startArticleId); 67 | 68 | @GET("/v1/article/{articleId}") 69 | Observable
loadArticle(@Path("articleId") String articleId); 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/CommaSeparatedParam.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import io.kaif.mobile.util.StringUtils; 7 | 8 | public class CommaSeparatedParam { 9 | 10 | private String[] params; 11 | 12 | public static CommaSeparatedParam of(List params) { 13 | return new CommaSeparatedParam(params.toArray(new String[params.size()])); 14 | } 15 | 16 | public CommaSeparatedParam(String[] params) { 17 | this.params = params; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return StringUtils.join(",", params); 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) { 28 | return true; 29 | } 30 | if (o == null || getClass() != o.getClass()) { 31 | return false; 32 | } 33 | 34 | CommaSeparatedParam that = (CommaSeparatedParam) o; 35 | 36 | // Probably incorrect - comparing Object[] arrays with Arrays.equals 37 | return Arrays.equals(params, that.params); 38 | 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return Arrays.hashCode(params); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/DebateService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.List; 4 | 5 | import io.kaif.mobile.model.Debate; 6 | import io.kaif.mobile.model.DebateNode; 7 | import retrofit2.http.Body; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.PUT; 10 | import retrofit2.http.Path; 11 | import retrofit2.http.Query; 12 | import rx.Observable; 13 | 14 | public interface DebateService { 15 | 16 | class CreateDebateEntry { 17 | String articleId; 18 | String parentDebateId; 19 | String content; 20 | 21 | public CreateDebateEntry(String articleId, String parentDebateId, String content) { 22 | this.articleId = articleId; 23 | this.parentDebateId = parentDebateId; 24 | this.content = content; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "CreateDebateEntry{" + 30 | "articleId='" + articleId + '\'' + 31 | ", parentDebateId='" + parentDebateId + '\'' + 32 | ", content='" + content + '\'' + 33 | '}'; 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) { 39 | return true; 40 | } 41 | if (o == null || getClass() != o.getClass()) { 42 | return false; 43 | } 44 | 45 | CreateDebateEntry that = (CreateDebateEntry) o; 46 | 47 | if (!articleId.equals(that.articleId)) { 48 | return false; 49 | } 50 | if (parentDebateId != null 51 | ? !parentDebateId.equals(that.parentDebateId) 52 | : that.parentDebateId != null) { 53 | return false; 54 | } 55 | return content.equals(that.content); 56 | 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | int result = articleId.hashCode(); 62 | result = 31 * result + (parentDebateId != null ? parentDebateId.hashCode() : 0); 63 | result = 31 * result + content.hashCode(); 64 | return result; 65 | } 66 | } 67 | 68 | @GET("/v1/debate/latest") 69 | Observable> listLatestDebates(@Query("start-debate-id") String startDebateId); 70 | 71 | 72 | @GET("/v1/debate/article/{articleId}/tree") 73 | Observable getDebateTree(@Path("articleId") String articleId); 74 | 75 | @PUT("/v1/debate") 76 | Observable debate(@Body CreateDebateEntry createDebateEntry); 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/FeedService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.List; 4 | 5 | import io.kaif.mobile.model.FeedAsset; 6 | import retrofit2.http.Body; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.POST; 9 | import retrofit2.http.Query; 10 | import rx.Observable; 11 | 12 | public interface FeedService { 13 | 14 | class AcknowledgeEntry { 15 | String assetId; 16 | 17 | public AcknowledgeEntry(String assetId) { 18 | this.assetId = assetId; 19 | } 20 | 21 | @Override 22 | public boolean equals(Object o) { 23 | if (this == o) { 24 | return true; 25 | } 26 | if (o == null || getClass() != o.getClass()) { 27 | return false; 28 | } 29 | 30 | AcknowledgeEntry that = (AcknowledgeEntry) o; 31 | 32 | return assetId.equals(that.assetId); 33 | 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return assetId.hashCode(); 39 | } 40 | } 41 | 42 | @POST("/v1/feed/acknowledge") 43 | Observable acknowledge(@Body AcknowledgeEntry acknowledgeEntry); 44 | 45 | @GET("/v1/feed/news") 46 | Observable> news(@Query("start-asset-id") String startAssetId); 47 | 48 | @GET("/v1/feed/news-unread-count") 49 | Observable newsUnreadCount(); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/OauthService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import io.kaif.mobile.model.oauth.AccessTokenInfo; 4 | import retrofit2.http.Field; 5 | import retrofit2.http.FormUrlEncoded; 6 | import retrofit2.http.POST; 7 | import rx.Observable; 8 | 9 | public interface OauthService { 10 | 11 | @FormUrlEncoded 12 | @POST("/oauth/access-token") 13 | Observable getAccessToken(@Field("client_id") String clientId, 14 | @Field("client_secret") String clientSecret, 15 | @Field("code") String code, 16 | @Field("redirect_uri") String redirectUri, 17 | @Field("grant_type") String grantType); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/VoteService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.List; 4 | 5 | import io.kaif.mobile.model.Vote; 6 | import retrofit2.http.Body; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.POST; 9 | import retrofit2.http.Query; 10 | import rx.Observable; 11 | 12 | public interface VoteService { 13 | 14 | class VoteArticleEntry { 15 | String articleId; 16 | Vote.VoteState voteState; 17 | 18 | public VoteArticleEntry(String articleId, Vote.VoteState voteState) { 19 | this.articleId = articleId; 20 | this.voteState = voteState; 21 | } 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) { 26 | return true; 27 | } 28 | if (o == null || getClass() != o.getClass()) { 29 | return false; 30 | } 31 | 32 | VoteArticleEntry that = (VoteArticleEntry) o; 33 | 34 | if (!articleId.equals(that.articleId)) { 35 | return false; 36 | } 37 | return voteState == that.voteState; 38 | 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | int result = articleId.hashCode(); 44 | result = 31 * result + voteState.hashCode(); 45 | return result; 46 | } 47 | } 48 | 49 | class VoteDebateEntry { 50 | String debateId; 51 | Vote.VoteState voteState; 52 | 53 | public VoteDebateEntry(String debateId, Vote.VoteState voteState) { 54 | this.debateId = debateId; 55 | this.voteState = voteState; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) { 61 | return true; 62 | } 63 | if (o == null || getClass() != o.getClass()) { 64 | return false; 65 | } 66 | 67 | VoteDebateEntry that = (VoteDebateEntry) o; 68 | 69 | if (!debateId.equals(that.debateId)) { 70 | return false; 71 | } 72 | return voteState == that.voteState; 73 | 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | int result = debateId.hashCode(); 79 | result = 31 * result + voteState.hashCode(); 80 | return result; 81 | } 82 | } 83 | 84 | @GET("/v1/vote/article") 85 | Observable> listArticleVotes( 86 | @Query(value = "article-id") CommaSeparatedParam articleIds); 87 | 88 | @GET("/v1/vote/debate") 89 | Observable> listDebateVotes( 90 | @Query(value = "debate-id") CommaSeparatedParam articleIds); 91 | 92 | @POST("/v1/vote/article") 93 | Observable voteArticle(@Body VoteArticleEntry voteArticleEntry); 94 | 95 | @POST("/v1/vote/debate") 96 | Observable voteDebate(@Body VoteDebateEntry voteDebateEntry); 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/service/ZoneService.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.service; 2 | 3 | import java.util.List; 4 | 5 | import io.kaif.mobile.model.Zone; 6 | import retrofit2.http.GET; 7 | import rx.Observable; 8 | 9 | public interface ZoneService { 10 | 11 | @GET("/v1/zone/all") 12 | Observable> listAll(); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.util; 2 | 3 | public class StringUtils { 4 | 5 | /** 6 | * copy from TextUtils 7 | */ 8 | public static String join(CharSequence delimiter, Object[] tokens) { 9 | StringBuilder sb = new StringBuilder(); 10 | boolean firstTime = true; 11 | for (Object token : tokens) { 12 | if (firstTime) { 13 | firstTime = false; 14 | } else { 15 | sb.append(delimiter); 16 | } 17 | sb.append(token); 18 | } 19 | return sb.toString(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/util/UtilModule.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.util; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import com.google.gson.Gson; 6 | import com.squareup.leakcanary.LeakCanary; 7 | import com.squareup.leakcanary.RefWatcher; 8 | 9 | import android.app.Application; 10 | import android.content.Context; 11 | import android.content.SharedPreferences; 12 | import android.net.ConnectivityManager; 13 | import android.preference.PreferenceManager; 14 | import dagger.Module; 15 | import dagger.Provides; 16 | 17 | @Module 18 | public class UtilModule { 19 | 20 | private final Application application; 21 | 22 | public UtilModule(Application application) { 23 | this.application = application; 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | Context provideApplicationContext() { 29 | return this.application; 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | Gson provideGson() { 35 | return new Gson(); 36 | } 37 | 38 | @Provides 39 | @Singleton 40 | SharedPreferences provideSharedPreferences() { 41 | return PreferenceManager.getDefaultSharedPreferences(application); 42 | } 43 | 44 | @Provides 45 | @Singleton 46 | ConnectivityManager provideConnectivityManager() { 47 | return (ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE); 48 | } 49 | 50 | @Provides 51 | @Singleton 52 | RefWatcher provideRefWatcher() { 53 | return LeakCanary.install(application); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/HomeActivity.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.widget.Toolbar; 7 | import android.widget.Toast; 8 | 9 | import javax.inject.Inject; 10 | 11 | import butterknife.BindView; 12 | import butterknife.ButterKnife; 13 | import io.kaif.mobile.KaifApplication; 14 | import io.kaif.mobile.R; 15 | import io.kaif.mobile.app.BaseActivity; 16 | import io.kaif.mobile.event.account.SignOutEvent; 17 | import io.kaif.mobile.view.daemon.AccountDaemon; 18 | 19 | public class HomeActivity extends BaseActivity { 20 | 21 | @Inject 22 | AccountDaemon accountDaemon; 23 | 24 | @BindView(R.id.tool_bar) 25 | Toolbar toolbar; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_home); 31 | ButterKnife.bind(this); 32 | KaifApplication.getInstance().beans().inject(this); 33 | setSupportActionBar(toolbar); 34 | 35 | if (!accountDaemon.hasAccount()) { 36 | showLoginActivityAndFinish(); 37 | return; 38 | } 39 | 40 | bind(accountDaemon.getSubject(SignOutEvent.class)).subscribe(accountEvent -> { 41 | Toast.makeText(this, R.string.sign_out_success, Toast.LENGTH_SHORT).show(); 42 | showLoginActivityAndFinish(); 43 | }); 44 | 45 | if (savedInstanceState == null) { 46 | final Fragment fragment = getSupportFragmentManager().findFragmentByTag("hot"); 47 | if (fragment == null) { 48 | getSupportFragmentManager().beginTransaction() 49 | .replace(R.id.container, HomeFragment.newInstance(), "hot") 50 | .commit(); 51 | } 52 | } 53 | } 54 | 55 | private void showLoginActivityAndFinish() { 56 | startActivity(new Intent(this, LoginActivity.class)); 57 | finish(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/HomeFragment.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.TabLayout; 5 | import android.support.v4.view.ViewPager; 6 | import android.view.LayoutInflater; 7 | import android.view.Menu; 8 | import android.view.MenuInflater; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import javax.inject.Inject; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import io.kaif.mobile.KaifApplication; 18 | import io.kaif.mobile.R; 19 | import io.kaif.mobile.app.BaseFragment; 20 | import io.kaif.mobile.view.daemon.AccountDaemon; 21 | import io.kaif.mobile.view.daemon.FeedDaemon; 22 | import io.kaif.mobile.view.drawable.NewsFeedBadgeDrawable; 23 | 24 | public class HomeFragment extends BaseFragment { 25 | 26 | public static HomeFragment newInstance() { 27 | return new HomeFragment(); 28 | } 29 | 30 | @BindView(R.id.sliding_tabs) 31 | TabLayout slidingTabLayout; 32 | 33 | @BindView(R.id.view_pager) 34 | ViewPager viewPager; 35 | 36 | @Inject 37 | AccountDaemon accountDaemon; 38 | 39 | @Inject 40 | FeedDaemon feedDaemon; 41 | 42 | private NewsFeedBadgeDrawable newsFeedBadgeDrawable; 43 | 44 | public HomeFragment() { 45 | } 46 | 47 | @Override 48 | public void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | KaifApplication.getInstance().beans().inject(this); 51 | setHasOptionsMenu(true); 52 | newsFeedBadgeDrawable = new NewsFeedBadgeDrawable(getResources()); 53 | } 54 | 55 | @Override 56 | public void onResume() { 57 | super.onResume(); 58 | bind(feedDaemon.newsUnreadCount()).subscribe(newsFeedBadgeDrawable::changeCount, ignoreEx -> { 59 | 60 | }); 61 | } 62 | 63 | @Override 64 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 65 | inflater.inflate(R.menu.menu_home, menu); 66 | MenuItem newsFeedAction = menu.findItem(R.id.action_news_feed); 67 | newsFeedAction.setIcon(newsFeedBadgeDrawable); 68 | super.onCreateOptionsMenu(menu, inflater); 69 | } 70 | 71 | @Override 72 | public boolean onOptionsItemSelected(MenuItem item) { 73 | int id = item.getItemId(); 74 | if (id == R.id.action_sign_out) { 75 | accountDaemon.signOut(); 76 | return true; 77 | } 78 | if (id == R.id.action_news_feed) { 79 | startActivity(new NewsFeedActivity.NewsFeedActivityIntent(getActivity())); 80 | return true; 81 | } 82 | return super.onOptionsItemSelected(item); 83 | } 84 | 85 | @Override 86 | public View onCreateView(LayoutInflater inflater, 87 | ViewGroup container, 88 | Bundle savedInstanceState) { 89 | View view = inflater.inflate(R.layout.fragment_home, container, false); 90 | ButterKnife.bind(this, view); 91 | 92 | viewPager.setAdapter(new HomePagerAdapter(getActivity(), getFragmentManager())); 93 | viewPager.setOffscreenPageLimit(2); 94 | slidingTabLayout.setBackgroundColor(getResources().getColor(R.color.kaif_blue)); 95 | slidingTabLayout.setupWithViewPager(viewPager); 96 | viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(slidingTabLayout)); 97 | 98 | return view; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/HomePagerAdapter.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v4.app.FragmentPagerAdapter; 7 | import io.kaif.mobile.R; 8 | 9 | public class HomePagerAdapter extends FragmentPagerAdapter { 10 | 11 | private Context context; 12 | 13 | public HomePagerAdapter(Context context, FragmentManager fm) { 14 | super(fm); 15 | this.context = context; 16 | } 17 | 18 | @Override 19 | public int getCount() { 20 | return 3; 21 | } 22 | 23 | @Override 24 | public Fragment getItem(int position) { 25 | if (position == 0) { 26 | return ArticlesFragment.newInstance(true); 27 | } else if (position == 1) { 28 | return ArticlesFragment.newInstance(false); 29 | } else { 30 | return LatestDebatesFragment.newInstance(); 31 | } 32 | } 33 | 34 | @Override 35 | public CharSequence getPageTitle(int position) { 36 | switch (position) { 37 | case 0: 38 | return context.getString(R.string.hot); 39 | case 1: 40 | return context.getString(R.string.latest); 41 | case 2: 42 | return context.getString(R.string.debate); 43 | default: 44 | return null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/LatestDebateListAdapter.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.text.format.DateUtils; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import butterknife.BindView; 15 | import butterknife.ButterKnife; 16 | import io.kaif.mobile.R; 17 | import io.kaif.mobile.kmark.KmarkProcessor; 18 | import io.kaif.mobile.view.viewmodel.DebateViewModel; 19 | import io.kaif.mobile.view.widget.ClickableSpanTouchListener; 20 | 21 | public class LatestDebateListAdapter extends RecyclerView.Adapter { 22 | 23 | static class DebateViewHolder extends RecyclerView.ViewHolder { 24 | 25 | @BindView(R.id.content) 26 | public TextView content; 27 | @BindView(R.id.last_update_time) 28 | public TextView lastUpdateTime; 29 | @BindView(R.id.vote_score) 30 | public TextView voteScore; 31 | @BindView(R.id.debater_name) 32 | public TextView debaterName; 33 | @BindView(R.id.zone) 34 | public TextView zone; 35 | 36 | public DebateViewHolder(View itemView) { 37 | super(itemView); 38 | ButterKnife.bind(this, itemView); 39 | content.setOnTouchListener(new ClickableSpanTouchListener()); 40 | } 41 | 42 | public void update(DebateViewModel debateViewModel) { 43 | final Context context = itemView.getContext(); 44 | debaterName.setText(debateViewModel.getDebaterName()); 45 | content.setText(KmarkProcessor.process(context, debateViewModel.getContent())); 46 | lastUpdateTime.setText(DateUtils.getRelativeTimeSpanString(debateViewModel.getLastUpdateTime() 47 | .getTime(), System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE)); 48 | voteScore.setText(String.valueOf(debateViewModel.getVoteScore())); 49 | zone.setText(itemView.getContext().getString(R.string.zone_path, debateViewModel.getZone())); 50 | } 51 | 52 | } 53 | 54 | private final List debates; 55 | 56 | private boolean hasNextPage; 57 | 58 | public LatestDebateListAdapter() { 59 | this.debates = new ArrayList<>(); 60 | } 61 | 62 | @Override 63 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 64 | final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); 65 | if (viewType == R.layout.item_loading) { 66 | return new RecyclerView.ViewHolder(view) { 67 | }; 68 | } 69 | return new DebateViewHolder(view); 70 | } 71 | 72 | @Override 73 | public int getItemViewType(int position) { 74 | if (position >= debates.size()) { 75 | return R.layout.item_loading; 76 | } 77 | return R.layout.item_debate_latest; 78 | } 79 | 80 | @Override 81 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 82 | if (position >= debates.size()) { 83 | return; 84 | } 85 | DebateViewModel debateVm = debates.get(position); 86 | DebateViewHolder debateViewHolder = (DebateViewHolder) holder; 87 | debateViewHolder.update(debateVm); 88 | } 89 | 90 | @Override 91 | public int getItemCount() { 92 | return debates.size() + (hasNextPage ? 1 : 0); 93 | } 94 | 95 | public void refresh(List debates) { 96 | this.debates.clear(); 97 | this.debates.addAll(debates); 98 | hasNextPage = !debates.isEmpty(); 99 | notifyDataSetChanged(); 100 | } 101 | 102 | public void addAll(List debates) { 103 | if (debates.isEmpty()) { 104 | hasNextPage = false; 105 | return; 106 | } 107 | this.debates.addAll(debates); 108 | notifyItemRangeInserted(this.debates.size() - debates.size(), debates.size()); 109 | } 110 | 111 | public String getLastDebateId() { 112 | return debates.get(debates.size() - 1).getDebateId(); 113 | } 114 | 115 | public DebateViewModel findItem(int position) { 116 | if (position >= debates.size()) { 117 | return null; 118 | } 119 | return debates.get(position); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.content.Intent; 4 | import android.graphics.Rect; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.ProgressBar; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import javax.inject.Inject; 14 | 15 | import butterknife.BindView; 16 | import butterknife.ButterKnife; 17 | import io.kaif.mobile.KaifApplication; 18 | import io.kaif.mobile.R; 19 | import io.kaif.mobile.app.BaseActivity; 20 | import io.kaif.mobile.view.daemon.AccountDaemon; 21 | import io.kaif.mobile.view.graphics.drawable.Triangle; 22 | import io.kaif.mobile.view.util.Views; 23 | 24 | public class LoginActivity extends BaseActivity { 25 | 26 | @Inject 27 | AccountDaemon accountDaemon; 28 | 29 | @BindView(R.id.sign_in) 30 | Button signInBtn; 31 | 32 | @BindView(R.id.sign_in_progress) 33 | ProgressBar signInProgress; 34 | 35 | @BindView(R.id.sign_in_title) 36 | TextView signInTitle; 37 | 38 | @BindView(R.id.title) 39 | TextView title; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_login); 45 | 46 | ButterKnife.bind(this); 47 | KaifApplication.getInstance().beans().inject(this); 48 | 49 | int triangleSize = (int) -title.getPaint().ascent(); 50 | Triangle triangle = new Triangle(getResources().getColor(R.color.vote_state_up), false); 51 | triangle.setBounds(new Rect(0, 0, triangleSize, triangleSize)); 52 | title.setCompoundDrawables(triangle, null, null, null); 53 | title.setCompoundDrawablePadding((int) Views.convertDpToPixel(16, this)); 54 | 55 | signInBtn.setOnClickListener(v -> startActivity(accountDaemon.createOauthPageIntent())); 56 | } 57 | 58 | @Override 59 | protected void onNewIntent(Intent intent) { 60 | super.onNewIntent(intent); 61 | setIntent(intent); 62 | } 63 | 64 | @Override 65 | protected void onResume() { 66 | super.onResume(); 67 | final Uri data = getIntent().getData(); 68 | if (data == null) { 69 | return; 70 | } 71 | getIntent().setData(null); 72 | 73 | signInProgress.setVisibility(View.VISIBLE); 74 | signInTitle.setVisibility(View.VISIBLE); 75 | signInBtn.setVisibility(View.GONE); 76 | bind(accountDaemon.accessToken(data.getQueryParameter("code"), data.getQueryParameter("state"))) 77 | .subscribe(aVoid -> { 78 | Toast.makeText(this, R.string.sign_in_success, Toast.LENGTH_SHORT).show(); 79 | Intent intent = new Intent(this, HomeActivity.class); 80 | startActivity(intent); 81 | finish(); 82 | }, throwable -> { 83 | Toast.makeText(this, throwable.toString(), Toast.LENGTH_SHORT).show(); 84 | signInProgress.setVisibility(View.GONE); 85 | signInTitle.setVisibility(View.GONE); 86 | signInBtn.setVisibility(View.VISIBLE); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/NewsFeedActivity.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.support.v4.app.NavUtils; 8 | import android.support.v4.app.TaskStackBuilder; 9 | import android.support.v7.widget.Toolbar; 10 | import android.view.MenuItem; 11 | 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | import io.kaif.mobile.KaifApplication; 15 | import io.kaif.mobile.R; 16 | import io.kaif.mobile.app.BaseActivity; 17 | 18 | public class NewsFeedActivity extends BaseActivity { 19 | 20 | //TODO 21 | @SuppressLint("ParcelCreator") 22 | static class NewsFeedActivityIntent extends Intent { 23 | 24 | NewsFeedActivityIntent(Context context) { 25 | super(context, NewsFeedActivity.class); 26 | } 27 | 28 | } 29 | 30 | @BindView(R.id.tool_bar) 31 | Toolbar toolbar; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_news_feed); 37 | ButterKnife.bind(this); 38 | KaifApplication.getInstance().beans().inject(this); 39 | setSupportActionBar(toolbar); 40 | setTitle(R.string.news_feed); 41 | 42 | //noinspection ConstantConditions 43 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 44 | } 45 | 46 | @Override 47 | public boolean onOptionsItemSelected(MenuItem item) { 48 | switch (item.getItemId()) { 49 | case android.R.id.home: 50 | Intent upIntent = NavUtils.getParentActivityIntent(this); 51 | if (NavUtils.shouldUpRecreateTask(this, upIntent)) { 52 | TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities(); 53 | } else { 54 | upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 55 | NavUtils.navigateUpTo(this, upIntent); 56 | } 57 | return true; 58 | } 59 | return super.onOptionsItemSelected(item); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/NewsFeedActivityFragment.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.widget.SwipeRefreshLayout; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.text.TextUtils; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import java.util.List; 14 | 15 | import javax.inject.Inject; 16 | 17 | import butterknife.BindView; 18 | import butterknife.ButterKnife; 19 | import io.kaif.mobile.KaifApplication; 20 | import io.kaif.mobile.R; 21 | import io.kaif.mobile.app.BaseFragment; 22 | import io.kaif.mobile.view.daemon.FeedDaemon; 23 | import io.kaif.mobile.view.viewmodel.FeedAssetViewModel; 24 | import io.kaif.mobile.view.widget.OnScrollToLastListener; 25 | import rx.Observable; 26 | 27 | public class NewsFeedActivityFragment extends BaseFragment { 28 | 29 | @BindView(R.id.debate_list) 30 | RecyclerView debateListView; 31 | 32 | @BindView(R.id.pull_to_refresh) 33 | SwipeRefreshLayout pullToRefreshLayout; 34 | 35 | @Inject 36 | FeedDaemon feedDaemon; 37 | 38 | private NewsFeedListAdapter adapter; 39 | 40 | public NewsFeedActivityFragment() { 41 | } 42 | 43 | @Override 44 | public void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | KaifApplication.getInstance().beans().inject(this); 47 | } 48 | 49 | @Override 50 | public View onCreateView(LayoutInflater inflater, 51 | ViewGroup container, 52 | Bundle savedInstanceState) { 53 | View view = inflater.inflate(R.layout.fragment_news_feed, container, false); 54 | ButterKnife.bind(this, view); 55 | setupView(); 56 | fillContent(); 57 | return view; 58 | } 59 | 60 | private void fillContent() { 61 | reload(); 62 | } 63 | 64 | private void setupView() { 65 | final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); 66 | debateListView.setLayoutManager(linearLayoutManager); 67 | adapter = new NewsFeedListAdapter(); 68 | adapter.setOnItemClickListener(debateViewModel -> { 69 | Intent intent = DebatesActivity.DebatesActivityIntent.create(getActivity(), debateViewModel); 70 | startActivity(intent); 71 | }); 72 | 73 | debateListView.setAdapter(adapter); 74 | debateListView.getItemAnimator().setChangeDuration(120); 75 | debateListView.addOnScrollListener(new OnScrollToLastListener() { 76 | private boolean loadingNextPage = false; 77 | 78 | @Override 79 | public void onScrollToLast() { 80 | if (loadingNextPage) { 81 | return; 82 | } 83 | loadingNextPage = true; 84 | bind(listFeedAssets(adapter.getLastAssetId())).subscribe(adapter::addAll, throwable -> { 85 | }, () -> loadingNextPage = false); 86 | } 87 | }); 88 | pullToRefreshLayout.setOnRefreshListener(this::reload); 89 | } 90 | 91 | private void reload() { 92 | pullToRefreshLayout.setRefreshing(true); 93 | bind(listFeedAssets(null)).subscribe(adapter::refresh, throwable -> { 94 | }, () -> pullToRefreshLayout.setRefreshing(false)); 95 | } 96 | 97 | private Observable> listFeedAssets(String feedAssetId) { 98 | if (TextUtils.isEmpty(feedAssetId)) { 99 | return feedDaemon.listAndAcknowledgeIfRequired(); 100 | } 101 | return feedDaemon.listNewsFeed(feedAssetId); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/animation/VoteAnimation.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.animation; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ArgbEvaluator; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.ValueAnimator; 8 | import android.content.Context; 9 | import android.graphics.PorterDuff; 10 | import android.view.View; 11 | import android.widget.TextView; 12 | import io.kaif.mobile.R; 13 | 14 | public class VoteAnimation { 15 | 16 | private static ValueAnimator colorChangeAnimation(View view, int from, int to) { 17 | ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), from, to); 18 | colorAnimation.addUpdateListener(animation -> { 19 | final Integer color = (Integer) animation.getAnimatedValue(); 20 | view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN); 21 | view.invalidate(); 22 | }); 23 | return colorAnimation; 24 | } 25 | 26 | public static Animator voteUpAnimation(View view) { 27 | Context context = view.getContext(); 28 | ValueAnimator colorAnimation = colorChangeAnimation(view, 29 | context.getResources().getColor(R.color.vote_state_empty), 30 | context.getResources().getColor(R.color.vote_state_up)); 31 | AnimatorSet animatorSet = new AnimatorSet(); 32 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 0, 360f); 33 | animatorSet.play(colorAnimation).with(rotateAnimation); 34 | animatorSet.setDuration(300); 35 | return animatorSet; 36 | } 37 | 38 | public static Animator voteDownAnimation(View view) { 39 | Context context = view.getContext(); 40 | ValueAnimator colorAnimation = colorChangeAnimation(view, 41 | context.getResources().getColor(R.color.vote_state_empty), 42 | context.getResources().getColor(R.color.vote_state_down)); 43 | AnimatorSet animatorSet = new AnimatorSet(); 44 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 0, -360f); 45 | animatorSet.play(colorAnimation).with(rotateAnimation); 46 | animatorSet.setDuration(300); 47 | return animatorSet; 48 | } 49 | 50 | public static Animator voteUpReverseAnimation(View view) { 51 | Context context = view.getContext(); 52 | ValueAnimator colorAnimation = colorChangeAnimation(view, 53 | context.getResources().getColor(R.color.vote_state_up), 54 | context.getResources().getColor(R.color.vote_state_empty)); 55 | AnimatorSet animatorSet = new AnimatorSet(); 56 | 57 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", 360, 0f); 58 | animatorSet.play(colorAnimation).with(rotateAnimation); 59 | animatorSet.setDuration(300); 60 | return animatorSet; 61 | } 62 | 63 | public static Animator voteDownReverseAnimation(View view) { 64 | Context context = view.getContext(); 65 | ValueAnimator colorAnimation = colorChangeAnimation(view, 66 | context.getResources().getColor(R.color.vote_state_down), 67 | context.getResources().getColor(R.color.vote_state_empty)); 68 | AnimatorSet animatorSet = new AnimatorSet(); 69 | 70 | final ObjectAnimator rotateAnimation = ObjectAnimator.ofFloat(view, "rotation", -360, 0f); 71 | animatorSet.play(colorAnimation).with(rotateAnimation); 72 | animatorSet.setDuration(300); 73 | return animatorSet; 74 | } 75 | 76 | private static ValueAnimator colorChangeAnimation(TextView view, int from, int to) { 77 | ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), from, to); 78 | colorAnimation.addUpdateListener(animation -> { 79 | final Integer color = (Integer) animation.getAnimatedValue(); 80 | view.setTextColor(color); 81 | }); 82 | colorAnimation.setDuration(300); 83 | return colorAnimation; 84 | } 85 | 86 | public static ValueAnimator voteUpTextColorAnimation(TextView view) { 87 | Context context = view.getContext(); 88 | 89 | return colorChangeAnimation(view, 90 | context.getResources().getColor(R.color.vote_state_empty), 91 | context.getResources().getColor(R.color.vote_state_up)); 92 | } 93 | 94 | public static ValueAnimator voteUpReverseTextColorAnimation(TextView view) { 95 | Context context = view.getContext(); 96 | 97 | return colorChangeAnimation(view, 98 | context.getResources().getColor(R.color.vote_state_up), 99 | context.getResources().getColor(R.color.vote_state_empty)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/daemon/AccountDaemon.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import java.util.UUID; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | 8 | import android.content.Intent; 9 | import android.net.Uri; 10 | import android.text.TextUtils; 11 | import io.kaif.mobile.config.ApiConfiguration; 12 | import io.kaif.mobile.event.EventPublishSubject; 13 | import io.kaif.mobile.event.account.AccountEvent; 14 | import io.kaif.mobile.event.account.SignInSuccessEvent; 15 | import io.kaif.mobile.event.account.SignOutEvent; 16 | import io.kaif.mobile.model.oauth.AccessTokenManager; 17 | import io.kaif.mobile.service.OauthService; 18 | import rx.Observable; 19 | 20 | @Singleton 21 | public class AccountDaemon { 22 | 23 | private final EventPublishSubject subject; 24 | 25 | private final OauthService oauthService; 26 | 27 | private final AccessTokenManager accessTokenManager; 28 | 29 | private final ApiConfiguration apiConfiguration; 30 | 31 | private String state; 32 | 33 | @Inject 34 | AccountDaemon(OauthService oauthService, 35 | AccessTokenManager accessTokenManager, 36 | ApiConfiguration apiConfiguration) { 37 | this.oauthService = oauthService; 38 | this.accessTokenManager = accessTokenManager; 39 | this.apiConfiguration = apiConfiguration; 40 | this.subject = new EventPublishSubject<>(); 41 | } 42 | 43 | public Observable getSubject(Class... classes) { 44 | return subject.getSubject(classes); 45 | } 46 | 47 | public Observable getSubject(Class clazz) { 48 | return subject.getSubject(clazz); 49 | } 50 | 51 | public Intent createOauthPageIntent() { 52 | state = UUID.randomUUID().toString(); 53 | final Uri uri = Uri.parse(apiConfiguration.getEndPoint()) 54 | .buildUpon() 55 | .appendPath("oauth") 56 | .appendPath("authorize") 57 | .appendQueryParameter("client_id", apiConfiguration.getClientId()) 58 | .appendQueryParameter("response_type", "code") 59 | .appendQueryParameter("scope", "public feed article vote user debate") 60 | .appendQueryParameter("state", state) 61 | .appendQueryParameter("redirect_uri", apiConfiguration.getRedirectUri()) 62 | .build(); 63 | Intent intent = new Intent(Intent.ACTION_VIEW); 64 | intent.setData(uri); 65 | return intent; 66 | } 67 | 68 | public boolean hasAccount() { 69 | return accessTokenManager.hasAccount(); 70 | } 71 | 72 | public void signOut() { 73 | accessTokenManager.signOut(); 74 | subject.onNext(new SignOutEvent()); 75 | } 76 | 77 | public Observable accessToken(String code, String state) { 78 | if (!TextUtils.equals(this.state, state)) { 79 | return Observable.error(new IllegalStateException("receive a malicious response")); 80 | } 81 | this.state = null; 82 | return oauthService.getAccessToken(apiConfiguration.getClientId(), 83 | apiConfiguration.getClientSecret(), 84 | code, 85 | apiConfiguration.getRedirectUri(), 86 | "authorization_code").map(accessTokenInfo -> { 87 | accessTokenManager.saveAccount(accessTokenInfo); 88 | subject.onNext(new SignInSuccessEvent()); 89 | return null; 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/daemon/FeedDaemon.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | 9 | import io.kaif.mobile.IgnoreAllSubscriber; 10 | import io.kaif.mobile.model.FeedAsset; 11 | import io.kaif.mobile.service.FeedService; 12 | import io.kaif.mobile.view.viewmodel.FeedAssetViewModel; 13 | import rx.Observable; 14 | 15 | @Singleton 16 | public class FeedDaemon { 17 | 18 | private final FeedService feedService; 19 | 20 | @Inject 21 | FeedDaemon(FeedService feedService) { 22 | this.feedService = feedService; 23 | } 24 | 25 | public Observable newsUnreadCount() { 26 | return feedService.newsUnreadCount(); 27 | } 28 | 29 | public Observable> listAndAcknowledgeIfRequired() { 30 | return feedService.news(null).map(feedAssets -> { 31 | if (!feedAssets.isEmpty()) { 32 | feedService.acknowledge(new FeedService.AcknowledgeEntry(feedAssets.get(0).getAssetId())) 33 | .subscribe(new IgnoreAllSubscriber<>()); 34 | } 35 | return mapToViewModel(feedAssets); 36 | }); 37 | } 38 | 39 | private List mapToViewModel(List feedAssets) { 40 | List vms = new ArrayList<>(); 41 | boolean isRead = false; 42 | for (int i = 0; i < feedAssets.size(); i++) { 43 | FeedAsset feedAsset = feedAssets.get(i); 44 | isRead |= feedAsset.isAcknowledged(); 45 | vms.add(new FeedAssetViewModel(feedAsset, isRead)); 46 | } 47 | return vms; 48 | } 49 | 50 | public Observable> listNewsFeed(String feedAssetId) { 51 | return feedService.news(feedAssetId).map(this::mapToViewModel); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/daemon/VoteDaemon.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Singleton; 5 | 6 | import io.kaif.mobile.event.EventPublishSubject; 7 | import io.kaif.mobile.event.vote.VoteArticleSuccessEvent; 8 | import io.kaif.mobile.event.vote.VoteDebateSuccessEvent; 9 | import io.kaif.mobile.event.vote.VoteEvent; 10 | import io.kaif.mobile.model.Vote; 11 | import io.kaif.mobile.service.VoteService; 12 | import rx.Observable; 13 | 14 | @Singleton 15 | public class VoteDaemon { 16 | 17 | private final EventPublishSubject subject; 18 | 19 | private final VoteService voteService; 20 | 21 | public Observable getSubject(Class... classes) { 22 | return subject.getSubject(classes); 23 | } 24 | 25 | public Observable getSubject(Class clazz) { 26 | return subject.getSubject(clazz); 27 | } 28 | 29 | @Inject 30 | VoteDaemon(VoteService voteService) { 31 | this.voteService = voteService; 32 | this.subject = new EventPublishSubject<>(); 33 | } 34 | 35 | public void voteArticle(String articleId, Vote.VoteState prevState, Vote.VoteState voteState) { 36 | subject.onNext(new VoteArticleSuccessEvent(articleId, voteState)); 37 | voteService.voteArticle(new VoteService.VoteArticleEntry(articleId, voteState)) 38 | .subscribe(aVoid -> { 39 | //success do nothing 40 | }, throwable -> { 41 | subject.onNext(new VoteArticleSuccessEvent(articleId, prevState)); 42 | }); 43 | } 44 | 45 | public void voteDebate(String debateId, Vote.VoteState prevState, Vote.VoteState voteState) { 46 | subject.onNext(new VoteDebateSuccessEvent(debateId, voteState)); 47 | voteService.voteDebate(new VoteService.VoteDebateEntry(debateId, voteState)) 48 | .subscribe(aVoid -> { 49 | //success do nothing 50 | }, throwable -> { 51 | subject.onNext(new VoteDebateSuccessEvent(debateId, prevState)); 52 | }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/daemon/ZoneDaemon.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.daemon; 2 | 3 | import java.util.List; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Singleton; 7 | 8 | import io.kaif.mobile.BuildConfig; 9 | import io.kaif.mobile.model.Zone; 10 | import io.kaif.mobile.service.ZoneService; 11 | import rx.Observable; 12 | 13 | @Singleton 14 | public class ZoneDaemon { 15 | 16 | private final ZoneService zoneService; 17 | 18 | @Inject 19 | ZoneDaemon(ZoneService zoneService) { 20 | this.zoneService = zoneService; 21 | } 22 | 23 | public Observable> listAll() { 24 | 25 | Observable> result = zoneService.listAll(); 26 | if (BuildConfig.DEBUG) { 27 | return result.map(zones -> { 28 | zones.add(new Zone("test", "測試專區")); 29 | return zones; 30 | }); 31 | } 32 | return result; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/drawable/NewsFeedBadgeDrawable.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.drawable; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Canvas; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | import android.support.v4.content.res.ResourcesCompat; 11 | import android.text.Layout; 12 | import android.text.StaticLayout; 13 | import android.text.TextPaint; 14 | import io.kaif.mobile.R; 15 | import io.kaif.mobile.view.util.Views; 16 | 17 | public class NewsFeedBadgeDrawable extends Drawable { 18 | 19 | private long count; 20 | private TextPaint textPaint; 21 | private Paint circlePaint; 22 | private final Resources resources; 23 | private final Drawable icon; 24 | private StaticLayout textLayout; 25 | private int countRadius; 26 | 27 | public NewsFeedBadgeDrawable(Resources resources) { 28 | this.resources = resources; 29 | icon = ResourcesCompat.getDrawable(resources, R.drawable.ic_notifications_white, null); 30 | 31 | textPaint = new TextPaint(); 32 | textPaint.setTextSize((int) (11.0f * resources.getDisplayMetrics().density + 0.5f)); 33 | textPaint.setColor(resources.getColor(android.R.color.white)); 34 | textPaint.setAntiAlias(true); 35 | 36 | circlePaint = new Paint(); 37 | circlePaint.setColor(0xffff0000); 38 | circlePaint.setAntiAlias(true); 39 | 40 | changeCount(0); 41 | } 42 | 43 | @Override 44 | public void draw(Canvas canvas) { 45 | icon.draw(canvas); 46 | if (count == 0) { 47 | return; 48 | } 49 | canvas.save(); 50 | canvas.translate(Views.convertDpToPixel(24, resources), Views.convertDpToPixel(8, resources)); 51 | canvas.drawCircle(0, 0, countRadius, circlePaint); 52 | canvas.translate(-textLayout.getWidth() / 2 - 1, -textLayout.getHeight() / 2 - 1); 53 | textLayout.draw(canvas); 54 | canvas.restore(); 55 | 56 | } 57 | 58 | @Override 59 | public void setAlpha(int alpha) { 60 | icon.setAlpha(alpha); 61 | textPaint.setAlpha(alpha); 62 | circlePaint.setAlpha(alpha); 63 | } 64 | 65 | @Override 66 | public void setColorFilter(ColorFilter cf) { 67 | icon.setColorFilter(cf); 68 | textPaint.setColorFilter(cf); 69 | circlePaint.setColorFilter(cf); 70 | } 71 | 72 | @Override 73 | public int getIntrinsicHeight() { 74 | return (int) Views.convertDpToPixel(36, resources); 75 | } 76 | 77 | @Override 78 | public int getIntrinsicWidth() { 79 | return (int) Views.convertDpToPixel(36, resources); 80 | } 81 | 82 | @Override 83 | public int getOpacity() { 84 | return PixelFormat.TRANSLUCENT; 85 | } 86 | 87 | private String count() { 88 | return count > 10 ? "10+" : String.valueOf(count); 89 | } 90 | 91 | public final void changeCount(int count) { 92 | this.count = count; 93 | int textWidth = (int) (textPaint.measureText(count())); 94 | textLayout = new StaticLayout(count(), 95 | textPaint, 96 | textWidth, 97 | Layout.Alignment.ALIGN_CENTER, 98 | 1.0f, 99 | 0.0f, 100 | false); 101 | countRadius = (int) (Math.sqrt(Math.pow(textLayout.getWidth() / 2.0, 2) 102 | + Math.pow(textLayout.getHeight() / 2.0, 2))); 103 | } 104 | 105 | @Override 106 | protected void onBoundsChange(Rect bounds) { 107 | int size = (int) Views.convertDpToPixel(24, resources); 108 | int horizontalPadding = Math.max(0, (bounds.width() - size) / 2); 109 | int verticalPadding = Math.max(0, (bounds.height() - size) / 2); 110 | 111 | icon.setBounds(horizontalPadding, 112 | verticalPadding, 113 | Math.min(horizontalPadding + size, bounds.right), 114 | Math.min(verticalPadding + size, bounds.bottom)); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/graphics/drawable/LevelDrawable.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.graphics.drawable; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | import io.kaif.mobile.R; 11 | import io.kaif.mobile.view.util.Views; 12 | 13 | public class LevelDrawable extends Drawable { 14 | 15 | public static final int MAX_NESTED_LEVEL = 7; 16 | private Paint paint; 17 | private int alpha; 18 | private int innerLevel = 1; 19 | private int paddingDp; 20 | private int lineWidthDp; 21 | private int backgroundColor; 22 | private final int paddingVertical; 23 | 24 | public LevelDrawable(Context context, int innerLevel, int backgroundColor) { 25 | this.backgroundColor = backgroundColor; 26 | this.paddingDp = (int) Views.convertDpToPixel(12, context); 27 | this.paddingVertical = (int) Views.convertDpToPixel(4, context); 28 | this.lineWidthDp = (int) Views.convertDpToPixel(2, context); 29 | this.innerLevel = Math.min(innerLevel, MAX_NESTED_LEVEL); 30 | this.paint = new Paint(); 31 | this.paint.setAntiAlias(true); 32 | this.paint.setColor(context.getResources().getColor(R.color.kaif_blue_light)); 33 | this.paint.setStyle(Paint.Style.FILL); 34 | this.paint.setStrokeWidth(lineWidthDp); 35 | } 36 | 37 | @Override 38 | public int getOpacity() { 39 | return PixelFormat.TRANSLUCENT; 40 | } 41 | 42 | @Override 43 | public void draw(Canvas canvas) { 44 | canvas.drawColor(backgroundColor); 45 | for (int i = 1; i < innerLevel; ++i) { 46 | final int x = paddingDp * i - lineWidthDp; 47 | canvas.drawLine(x, 0, x, canvas.getHeight(), paint); 48 | } 49 | } 50 | 51 | @Override 52 | public boolean getPadding(Rect padding) { 53 | padding.set(paddingDp * (innerLevel - 1) + lineWidthDp * 2, 54 | paddingVertical, 55 | lineWidthDp, 56 | paddingVertical); 57 | return true; 58 | } 59 | 60 | @Override 61 | public void setAlpha(int alpha) { 62 | this.alpha = alpha; 63 | } 64 | 65 | @Override 66 | public int getAlpha() { 67 | return alpha; 68 | } 69 | 70 | @Override 71 | public void setColorFilter(ColorFilter cf) { 72 | paint.setColorFilter(cf); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/graphics/drawable/Triangle.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.graphics.drawable; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.ColorFilter; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | import android.graphics.PixelFormat; 8 | import android.graphics.Rect; 9 | import android.graphics.drawable.Drawable; 10 | 11 | public class Triangle extends Drawable { 12 | 13 | private Paint paint; 14 | private int alpha; 15 | Path triangle; 16 | private boolean reverse; 17 | 18 | public Triangle(int color) { 19 | this(color, false); 20 | } 21 | 22 | public Triangle(int color, boolean reverse) { 23 | this.reverse = reverse; 24 | this.triangle = new Path(); 25 | 26 | this.paint = new Paint(); 27 | this.paint.setAntiAlias(true); 28 | this.paint.setColor(color); 29 | this.paint.setStyle(Paint.Style.FILL); 30 | } 31 | 32 | @Override 33 | public int getOpacity() { 34 | return PixelFormat.OPAQUE; 35 | } 36 | 37 | @Override 38 | public void draw(Canvas canvas) { 39 | canvas.drawPath(triangle, this.paint); 40 | } 41 | 42 | @Override 43 | protected void onBoundsChange(Rect bounds) { 44 | triangle.reset(); 45 | if (reverse) { 46 | triangle.moveTo(bounds.left + bounds.width() / 2f, bounds.bottom); 47 | triangle.lineTo(bounds.left + bounds.width(), bounds.top); 48 | triangle.lineTo(bounds.left, bounds.top); 49 | return; 50 | } 51 | triangle.moveTo(bounds.left + bounds.width() / 2f, bounds.top); 52 | triangle.lineTo(bounds.left + bounds.width(), bounds.bottom); 53 | triangle.lineTo(bounds.left, bounds.bottom); 54 | } 55 | 56 | @Override 57 | public void setAlpha(int alpha) { 58 | this.alpha = alpha; 59 | } 60 | 61 | @Override 62 | public int getAlpha() { 63 | return alpha; 64 | } 65 | 66 | @Override 67 | public void setColorFilter(ColorFilter cf) { 68 | paint.setColorFilter(cf); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/share/ShareArticleActivity.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.share; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.widget.Toolbar; 5 | import android.widget.Toast; 6 | 7 | import javax.inject.Inject; 8 | 9 | import butterknife.BindView; 10 | import butterknife.ButterKnife; 11 | import io.kaif.mobile.KaifApplication; 12 | import io.kaif.mobile.R; 13 | import io.kaif.mobile.app.BaseActivity; 14 | import io.kaif.mobile.view.daemon.AccountDaemon; 15 | 16 | public class ShareArticleActivity extends BaseActivity { 17 | 18 | @BindView(R.id.tool_bar) 19 | Toolbar toolbar; 20 | 21 | @Inject 22 | AccountDaemon accountDaemon; 23 | 24 | @Override 25 | public void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_share_article); 28 | ButterKnife.bind(this); 29 | 30 | KaifApplication.getInstance().beans().inject(this); 31 | setSupportActionBar(toolbar); 32 | 33 | if (!accountDaemon.hasAccount()) { 34 | Toast.makeText(this, R.string.not_sign_in_warning, Toast.LENGTH_SHORT).show(); 35 | finish(); 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/util/Views.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.Resources; 5 | import android.util.DisplayMetrics; 6 | 7 | public class Views { 8 | public static float convertDpToPixel(float dp, Context context) { 9 | return convertDpToPixel(dp, context.getResources()); 10 | } 11 | 12 | public static float convertDpToPixel(float dp, Resources resources) { 13 | 14 | DisplayMetrics metrics = resources.getDisplayMetrics(); 15 | return dp * (metrics.densityDpi / 160f); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/viewmodel/ArticleViewModel.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.viewmodel; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | import android.net.Uri; 7 | import io.kaif.mobile.model.Article; 8 | import io.kaif.mobile.model.Vote; 9 | 10 | public class ArticleViewModel implements Serializable { 11 | 12 | private Article article; 13 | 14 | private Vote vote; 15 | 16 | private Vote.VoteState prevVoteState; 17 | 18 | private Vote.VoteState currentVoteState; 19 | 20 | private boolean canShowVoteAnimation; 21 | 22 | public ArticleViewModel(Article article, Vote vote) { 23 | this.article = article; 24 | this.vote = vote; 25 | this.prevVoteState = vote.getVoteState(); 26 | this.currentVoteState = vote.getVoteState(); 27 | this.canShowVoteAnimation = true; 28 | } 29 | 30 | public String getZone() { 31 | return article.getZone(); 32 | } 33 | 34 | public Date getCreateTime() { 35 | return article.getCreateTime(); 36 | } 37 | 38 | public String getTitle() { 39 | return article.getTitle(); 40 | } 41 | 42 | public Article.ArticleType getArticleType() { 43 | return article.getArticleType(); 44 | } 45 | 46 | public long getScore() { 47 | return article.getUpVote() + currentVoteState.delta(vote.getVoteState()); 48 | } 49 | 50 | public String getLink() { 51 | return article.getLink(); 52 | } 53 | 54 | public long getDebateCount() { 55 | return article.getDebateCount(); 56 | } 57 | 58 | public String getZoneTitle() { 59 | return article.getZoneTitle(); 60 | } 61 | 62 | public String getArticleId() { 63 | return article.getArticleId(); 64 | } 65 | 66 | public String getContent() { 67 | return article.getContent(); 68 | } 69 | 70 | public String getAuthorName() { 71 | return article.getAuthorName(); 72 | } 73 | 74 | public void setCanShowVoteAnimation(boolean canShowVoteAnimation) { 75 | this.canShowVoteAnimation = canShowVoteAnimation; 76 | } 77 | 78 | public boolean shouldShowVoteEffect() { 79 | if (!canShowVoteAnimation) { 80 | return false; 81 | } 82 | 83 | if (currentVoteState == prevVoteState && currentVoteState == Vote.VoteState.EMPTY) { 84 | return false; 85 | } 86 | return true; 87 | } 88 | 89 | public void updateVoteState(Vote.VoteState voteState) { 90 | prevVoteState = currentVoteState; 91 | currentVoteState = voteState; 92 | canShowVoteAnimation = true; 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | if (this == o) { 98 | return true; 99 | } 100 | if (o == null || getClass() != o.getClass()) { 101 | return false; 102 | } 103 | 104 | ArticleViewModel that = (ArticleViewModel) o; 105 | 106 | return article.equals(that.article); 107 | 108 | } 109 | 110 | @Override 111 | public int hashCode() { 112 | return article.hashCode(); 113 | } 114 | 115 | public Vote.VoteState getCurrentVoeState() { 116 | return currentVoteState; 117 | } 118 | 119 | public Uri getPermaLink() { 120 | return new Uri.Builder().scheme("https") 121 | .authority("kaif.io") 122 | .appendPath("z") 123 | .appendPath(getZone()) 124 | .appendPath("debates") 125 | .appendPath(getArticleId()) 126 | .build(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/viewmodel/DebateViewModel.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.viewmodel; 2 | 3 | import java.util.Date; 4 | 5 | import io.kaif.mobile.model.Debate; 6 | import io.kaif.mobile.model.Vote; 7 | 8 | public class DebateViewModel { 9 | 10 | private final Debate debate; 11 | 12 | private final Vote vote; 13 | 14 | private Vote.VoteState prevVoteState; 15 | 16 | private Vote.VoteState currentVoteState; 17 | 18 | private boolean canShowVoteAnimation; 19 | 20 | public DebateViewModel(Debate debate, Vote vote) { 21 | this.debate = debate; 22 | this.vote = vote; 23 | this.prevVoteState = vote.getVoteState(); 24 | this.currentVoteState = vote.getVoteState(); 25 | canShowVoteAnimation = true; 26 | } 27 | 28 | public long getDownVote() { 29 | return debate.getDownVote(); 30 | } 31 | 32 | public String getDebaterName() { 33 | return debate.getDebaterName(); 34 | } 35 | 36 | public int getLevel() { 37 | return debate.getLevel(); 38 | } 39 | 40 | public Date getLastUpdateTime() { 41 | return debate.getLastUpdateTime(); 42 | } 43 | 44 | public String getDebateId() { 45 | return debate.getDebateId(); 46 | } 47 | 48 | public String getParentDebateId() { 49 | return debate.getParentDebateId(); 50 | } 51 | 52 | public String getZone() { 53 | return debate.getZone(); 54 | } 55 | 56 | public long getUpVote() { 57 | return debate.getUpVote(); 58 | } 59 | 60 | public String getContent() { 61 | return debate.getContent(); 62 | } 63 | 64 | public String getArticleId() { 65 | return debate.getArticleId(); 66 | } 67 | 68 | public Date getCreateTime() { 69 | return debate.getCreateTime(); 70 | } 71 | 72 | public Vote.VoteState getCurrentVoeState() { 73 | return currentVoteState; 74 | } 75 | 76 | public long getVoteScore() { 77 | return debate.getUpVote() - debate.getDownVote() 78 | + (currentVoteState.delta(vote.getVoteState())); 79 | } 80 | 81 | public void setCanShowVoteAnimation(boolean canShowVoteAnimation) { 82 | this.canShowVoteAnimation = canShowVoteAnimation; 83 | } 84 | 85 | public boolean shouldShowVoteEffect() { 86 | if (!canShowVoteAnimation) { 87 | return false; 88 | } 89 | if (currentVoteState == prevVoteState) { 90 | return false; 91 | } 92 | return true; 93 | } 94 | 95 | public void updateVoteState(Vote.VoteState voteState) { 96 | prevVoteState = this.currentVoteState; 97 | currentVoteState = voteState; 98 | canShowVoteAnimation = true; 99 | } 100 | 101 | public Vote.VoteState getPrevVoteState() { 102 | return prevVoteState; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/viewmodel/FeedAssetViewModel.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.viewmodel; 2 | 3 | import io.kaif.mobile.model.FeedAsset; 4 | import io.kaif.mobile.model.Vote; 5 | 6 | public class FeedAssetViewModel { 7 | 8 | private DebateViewModel debateViewModel; 9 | private FeedAsset feedAsset; 10 | private boolean read; 11 | 12 | public FeedAssetViewModel(FeedAsset feedAsset, boolean read) { 13 | this.feedAsset = feedAsset; 14 | this.read = read; 15 | //doesn't support inline vote yet, provide fake vote state. 16 | this.debateViewModel = new DebateViewModel(feedAsset.getDebate(), 17 | Vote.abstain(feedAsset.getDebate().getDebateId())); 18 | } 19 | 20 | public DebateViewModel getDebateViewModel() { 21 | return debateViewModel; 22 | } 23 | 24 | public String getAssetId() { 25 | return feedAsset.getAssetId(); 26 | } 27 | 28 | public boolean isRead() { 29 | return read; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/ArticleScoreTextView.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import android.animation.Animator; 4 | import android.content.Context; 5 | import android.util.AttributeSet; 6 | import android.widget.TextView; 7 | import io.kaif.mobile.model.Vote; 8 | import io.kaif.mobile.view.animation.VoteAnimation; 9 | 10 | public class ArticleScoreTextView extends TextView { 11 | 12 | private Vote.VoteState voteState; 13 | 14 | public ArticleScoreTextView(Context context) { 15 | this(context, null); 16 | } 17 | 18 | public ArticleScoreTextView(Context context, AttributeSet attrs) { 19 | super(context, attrs); 20 | } 21 | 22 | public ArticleScoreTextView(Context context, AttributeSet attrs, int defStyleAttr) { 23 | super(context, attrs, defStyleAttr); 24 | } 25 | 26 | public void update(long score, Vote.VoteState voteState) { 27 | this.voteState = voteState; 28 | setText(String.valueOf(score)); 29 | showVoteColor(false); 30 | } 31 | 32 | public void showVoteColor(boolean showAnimation) { 33 | final Animator animator; 34 | switch (voteState) { 35 | case UP: { 36 | animator = VoteAnimation.voteUpTextColorAnimation(this); 37 | break; 38 | } 39 | default: 40 | animator = VoteAnimation.voteUpReverseTextColorAnimation(this); 41 | break; 42 | } 43 | if (!showAnimation) { 44 | animator.setDuration(0); 45 | } 46 | animator.start(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/ClickableSpanTouchListener.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import android.text.Layout; 4 | import android.text.Spanned; 5 | import android.text.style.ClickableSpan; 6 | import android.view.MotionEvent; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | /** 11 | * fix url span can't click problem 12 | */ 13 | public class ClickableSpanTouchListener implements View.OnTouchListener { 14 | 15 | @Override 16 | public boolean onTouch(View v, MotionEvent event) { 17 | Spanned spanned = (Spanned) ((TextView) v).getText(); 18 | TextView widget = (TextView) v; 19 | int action = event.getAction(); 20 | 21 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 22 | int x = (int) event.getX(); 23 | int y = (int) event.getY(); 24 | 25 | x -= widget.getTotalPaddingLeft(); 26 | y -= widget.getTotalPaddingTop(); 27 | 28 | x += widget.getScrollX(); 29 | y += widget.getScrollY(); 30 | 31 | Layout layout = widget.getLayout(); 32 | int line = layout.getLineForVertical(y); 33 | int off = layout.getOffsetForHorizontal(line, x); 34 | 35 | ClickableSpan[] link = spanned.getSpans(off, off, ClickableSpan.class); 36 | 37 | if (link.length != 0) { 38 | if (action == MotionEvent.ACTION_UP) { 39 | link[0].onClick(widget); 40 | } 41 | return true; 42 | } 43 | } 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/OnScrollToLastListener.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import android.support.v7.widget.LinearLayoutManager; 4 | import android.support.v7.widget.RecyclerView; 5 | 6 | public abstract class OnScrollToLastListener extends RecyclerView.OnScrollListener { 7 | 8 | @Override 9 | public final void onScrolled(RecyclerView recyclerView, int dx, int dy) { 10 | LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); 11 | 12 | int visibleItemCount = layoutManager.getChildCount(); 13 | int totalItemCount = layoutManager.getItemCount(); 14 | int pastVisibleItems = layoutManager.findFirstVisibleItemPosition(); 15 | 16 | if ((visibleItemCount + pastVisibleItems) >= totalItemCount) { 17 | onScrollToLast(); 18 | } 19 | } 20 | 21 | public abstract void onScrollToLast(); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/OnVoteClickListener.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import io.kaif.mobile.model.Vote; 4 | 5 | public interface OnVoteClickListener { 6 | void onVoteClicked(Vote.VoteState from, Vote.VoteState to); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/ReplyDialog.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.annotation.NonNull; 7 | import android.support.v7.app.AlertDialog; 8 | import android.view.KeyEvent; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.inputmethod.EditorInfo; 12 | import android.widget.EditText; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | 16 | import com.trello.rxlifecycle.components.support.RxDialogFragment; 17 | 18 | import javax.inject.Inject; 19 | 20 | import butterknife.BindView; 21 | import butterknife.ButterKnife; 22 | import io.kaif.mobile.KaifApplication; 23 | import io.kaif.mobile.R; 24 | import io.kaif.mobile.view.daemon.DebateDaemon; 25 | 26 | public class ReplyDialog extends RxDialogFragment implements TextView.OnEditorActionListener { 27 | 28 | public static final int MIN_CONTENT_SIZE = 5; 29 | 30 | public static ReplyDialog createFragment(String article, String parentDebateId, int level) { 31 | ReplyDialog replyDialog = new ReplyDialog(); 32 | Bundle args = new Bundle(); 33 | args.putString("PARENT_DEBATE_ID", parentDebateId); 34 | args.putString("ARTICLE_ID", article); 35 | args.putInt("LEVEL", level); 36 | replyDialog.setArguments(args); 37 | return replyDialog; 38 | } 39 | 40 | private String getParentDebateId() { 41 | return getArguments().getString("PARENT_DEBATE_ID"); 42 | } 43 | 44 | private String getArticleId() { 45 | return getArguments().getString("ARTICLE_ID"); 46 | } 47 | 48 | private int getLevel() { 49 | return getArguments().getInt("LEVEL"); 50 | } 51 | 52 | @Inject 53 | DebateDaemon debateDaemon; 54 | 55 | @BindView(R.id.debate_content) 56 | protected EditText contentEditText; 57 | 58 | public ReplyDialog() { 59 | // Empty constructor required for DialogFragment 60 | } 61 | 62 | @Override 63 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 64 | if (EditorInfo.IME_ACTION_DONE == actionId) { 65 | submitDebate(); 66 | this.dismiss(); 67 | return true; 68 | } 69 | return false; 70 | } 71 | 72 | @Override 73 | public void onAttach(Activity activity) { 74 | super.onAttach(activity); 75 | KaifApplication.getInstance().beans().inject(this); 76 | } 77 | 78 | @NonNull 79 | @Override 80 | public Dialog onCreateDialog(Bundle savedInstanceState) { 81 | LayoutInflater inflater = LayoutInflater.from(getActivity()); 82 | View view = inflater.inflate(R.layout.fragment_reply, null); 83 | ButterKnife.bind(this, view); 84 | contentEditText.setOnEditorActionListener(this); 85 | return new AlertDialog.Builder(getActivity()).setTitle(R.string.reply) 86 | .setView(view) 87 | .setPositiveButton(R.string.submit_reply, (dialog, whichButton) -> { 88 | submitDebate(); 89 | }) 90 | .setNegativeButton(R.string.dialog_cancel, (dialog, whichButton) -> { 91 | }) 92 | .create(); 93 | } 94 | 95 | private void submitDebate() { 96 | String debateContent = contentEditText.getText().toString().trim(); 97 | if (debateContent.length() < MIN_CONTENT_SIZE) { 98 | Toast.makeText(getActivity(), R.string.debate_too_short, Toast.LENGTH_SHORT).show(); 99 | return; 100 | } 101 | debateDaemon.debate(getArticleId(), getParentDebateId(), getLevel(), debateContent); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/io/kaif/mobile/view/widget/VoteArticleButton.java: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view.widget; 2 | 3 | import android.animation.Animator; 4 | import android.content.Context; 5 | import android.graphics.drawable.InsetDrawable; 6 | import android.util.AttributeSet; 7 | import android.widget.Button; 8 | import io.kaif.mobile.R; 9 | import io.kaif.mobile.model.Vote; 10 | import io.kaif.mobile.view.animation.VoteAnimation; 11 | import io.kaif.mobile.view.graphics.drawable.Triangle; 12 | import io.kaif.mobile.view.util.Views; 13 | 14 | public class VoteArticleButton extends Button { 15 | 16 | private Vote.VoteState voteState; 17 | 18 | private OnVoteClickListener onVoteClickListener; 19 | 20 | public VoteArticleButton(Context context) { 21 | this(context, null); 22 | } 23 | 24 | public VoteArticleButton(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | init(context); 27 | } 28 | 29 | public VoteArticleButton(Context context, AttributeSet attrs, int defStyleAttr) { 30 | super(context, attrs, defStyleAttr); 31 | init(context); 32 | } 33 | 34 | private void init(Context context) { 35 | setBackground(new InsetDrawable(new Triangle(context.getResources() 36 | .getColor(R.color.vote_state_empty)), 37 | (int) Views.convertDpToPixel(12, context), 38 | (int) Views.convertDpToPixel(4, context), 39 | (int) Views.convertDpToPixel(12, context), 40 | (int) Views.convertDpToPixel(4, context))); 41 | voteState = Vote.VoteState.EMPTY; 42 | setOnClickListener(v -> { 43 | if (onVoteClickListener != null) { 44 | Vote.VoteState from = this.voteState; 45 | Vote.VoteState to = (this.voteState == Vote.VoteState.EMPTY 46 | ? Vote.VoteState.UP 47 | : Vote.VoteState.EMPTY); 48 | onVoteClickListener.onVoteClicked(from, to); 49 | } 50 | }); 51 | } 52 | 53 | public void setOnVoteClickListener(OnVoteClickListener onVoteClickListener) { 54 | this.onVoteClickListener = onVoteClickListener; 55 | } 56 | 57 | public void updateVoteState(Vote.VoteState voteState) { 58 | this.voteState = voteState; 59 | showVoteColor(false); 60 | } 61 | 62 | public void showVoteColor(boolean showAnimation) { 63 | final Animator animator; 64 | switch (voteState) { 65 | case UP: { 66 | animator = VoteAnimation.voteUpAnimation(this); 67 | break; 68 | } 69 | default: 70 | animator = VoteAnimation.voteUpReverseAnimation(this); 71 | break; 72 | } 73 | if (!showAnimation) { 74 | animator.setDuration(0); 75 | } 76 | animator.start(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/kaif/mobile/view/HonorActivity.kt: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.design.widget.AppBarLayout 7 | import io.kaif.mobile.R 8 | import io.kaif.mobile.app.BaseActivity 9 | import org.jetbrains.anko.* 10 | import org.jetbrains.anko.appcompat.v7.themedToolbar 11 | import org.jetbrains.anko.design.coordinatorLayout 12 | import org.jetbrains.anko.design.themedAppBarLayout 13 | 14 | class HonorActivity : BaseActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | HonorActivityUI().setContentView(this) 18 | setSupportActionBar(find(io.kaif.mobile.R.id.tool_bar)) 19 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 20 | 21 | if (savedInstanceState == null) { 22 | supportFragmentManager.beginTransaction() 23 | .replace(R.id.content_frame, HonorFragment.newInstance()) 24 | .commit() 25 | } 26 | } 27 | } 28 | 29 | class HonorActivityIntent(context: Context) : Intent(context, HonorActivity::class.java) 30 | 31 | class HonorActivityUI : AnkoComponent { 32 | override fun createView(ui: AnkoContext) = with(ui) { 33 | coordinatorLayout { 34 | lparams(width = matchParent, height = matchParent) 35 | themedAppBarLayout { 36 | lparams(width = matchParent, height = wrapContent) 37 | themedToolbar(R.style.ThemeOverlay_AppCompat_Dark_ActionBar) { 38 | id = io.kaif.mobile.R.id.tool_bar 39 | popupTheme = R.style.ThemeOverlay_AppCompat_Light 40 | title = resources.getString(R.string.honor) 41 | }.lparams(width = matchParent, height = wrapContent) { 42 | scrollFlags = R.id.enterAlways 43 | } 44 | } 45 | frameLayout { 46 | id = io.kaif.mobile.R.id.content_frame 47 | }.lparams(width = matchParent, height = matchParent) { 48 | behavior = AppBarLayout.ScrollingViewBehavior() 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/kaif/mobile/view/HonorAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.LinearLayout 7 | import android.widget.TextView 8 | import io.kaif.mobile.R 9 | import org.jetbrains.anko.* 10 | 11 | class HonorAdapter : RecyclerView.Adapter() { 12 | 13 | private val honors = arrayOf("123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789", "123", "456", "789") 14 | 15 | override fun getItemCount(): Int { 16 | return honors.size 17 | } 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): HonorViewHolder { 20 | return HonorViewHolder(HonorView().createView(AnkoContext.create(parent!!.context, parent))) 21 | } 22 | 23 | override fun onBindViewHolder(holder: HonorViewHolder?, position: Int) { 24 | holder!!.bind(honors[position]) 25 | } 26 | } 27 | 28 | class HonorView : AnkoComponent { 29 | override fun createView(ui: AnkoContext): View { 30 | return with(ui) { 31 | linearLayout { 32 | lparams(width = matchParent, height = dip(48)) 33 | orientation = LinearLayout.HORIZONTAL 34 | textView { 35 | id = R.id.title 36 | textSize = 16f 37 | }.lparams(width = matchParent, height = dip(48)) 38 | } 39 | } 40 | } 41 | } 42 | 43 | class HonorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 44 | var title: TextView? = itemView.findViewById(R.id.title) 45 | 46 | fun bind(data: String) { 47 | title?.text = "hi $data" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/kotlin/io/kaif/mobile/view/HonorFragment.kt: -------------------------------------------------------------------------------- 1 | package io.kaif.mobile.view 2 | 3 | import android.os.Bundle 4 | import android.support.v7.widget.LinearLayoutManager 5 | import android.support.v7.widget.RecyclerView 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import io.kaif.mobile.KaifApplication 10 | import io.kaif.mobile.app.BaseFragment 11 | import org.jetbrains.anko.AnkoComponent 12 | import org.jetbrains.anko.AnkoContext 13 | import org.jetbrains.anko.frameLayout 14 | import org.jetbrains.anko.matchParent 15 | import org.jetbrains.anko.recyclerview.v7.recyclerView 16 | import org.jetbrains.anko.support.v4.ctx 17 | 18 | class HonorFragment : BaseFragment() { 19 | 20 | companion object { 21 | fun newInstance(): HonorFragment { 22 | return HonorFragment() 23 | } 24 | } 25 | 26 | lateinit var honors: RecyclerView 27 | 28 | private lateinit var honorsAdapter: HonorAdapter 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | KaifApplication.getInstance().beans().inject(this) 33 | honorsAdapter = HonorAdapter() 34 | } 35 | 36 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 37 | return HonorFragmentUI(honorsAdapter).createView(AnkoContext.create(ctx, this)) 38 | } 39 | } 40 | 41 | 42 | class HonorFragmentUI(private val honorAdapter: HonorAdapter) : AnkoComponent { 43 | override fun createView(ui: AnkoContext) = with(ui) { 44 | frameLayout { 45 | lparams(width = matchParent, height = matchParent) 46 | owner.honors = recyclerView { 47 | lparams(width = matchParent, height = matchParent) 48 | layoutManager = LinearLayoutManager(ctx) 49 | adapter = honorAdapter 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/res/anim/rotate_vote.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/anim/rotate_vote_back.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/anim/scale_action_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notifications_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_notifications_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_open_in_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_open_in_browser.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_record_voice_over_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_record_voice_over_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_reply.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-hdpi/ic_send.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notifications_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_notifications_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_open_in_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_open_in_browser.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_record_voice_over_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_record_voice_over_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_reply.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xhdpi/ic_send.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notifications_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_notifications_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_open_in_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_open_in_browser.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_record_voice_over_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_record_voice_over_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_reply.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaif-open/kaif-android/f8321142792b8e9c9de38416e1b47b9bd6d3faa5/app/src/main/res/drawable-xxhdpi/ic_send.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/news_feed_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/news_feed_divider_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/news_feed_divider_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/score_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_debates.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_home.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 |