Reblogging and HTML text (#107)

* Changed Post interaction icons and added click feedback

* added reblog and unreblog api implementations

* Use fancy animated buttons

* WIP reposter

* WIP reblog button

* renamed ViewHolder => PostViewHolder

* activated reblogger in feed

* added custom html parser, still need to fix clickable links

* added parsed HTML in notifications

* fixed mention click

* added tests for reblog and clickable mentions

* adapted unit tests to work with new html parser

* changed incoherent comment

* made hashtags slightly less useless

* removed unit test that were no longer valid

* removed useless test

* trying to fix tests

* fixing tests

* trying to improve coverage a little

* removed unused code to improve coverage

* changed cast to type converter

* added failure responses to help coverage

* added mock server response for reblogging

* fixed broken json

* trying to fix a broken test

* Tweak tests

* Typo in test

* Add scrolls to make tests pass on small screens

* fixed old JSON in mockserver

* fixed linter issue

Co-authored-by: Matthieu <61561059+Wv5twkFEKh54vo4tta9yu7dHa3@users.noreply.github.com>
This commit is contained in:
Andrew Dobis 2020-04-23 17:48:45 +02:00 committed by GitHub
parent 8950d4c162
commit 8265ac2d62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1142 additions and 474 deletions

View File

@ -64,6 +64,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "androidx.browser:browser:1.2.0" implementation "androidx.browser:browser:1.2.0"
implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.connyduck:sparkbutton:3.0.0'
def room_version = "2.2.5" def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"

View File

@ -2,19 +2,39 @@ package com.h.pixeldroid
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.text.SpannableString
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.BundleMatchers.hasValue
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
import com.h.pixeldroid.testUtility.CustomMatchers
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
import com.h.pixeldroid.testUtility.MockServer import com.h.pixeldroid.testUtility.MockServer
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -32,7 +52,7 @@ class IntentTest {
@get:Rule @get:Rule
var mLoginActivityActivityTestRule = var mLoginActivityActivityTestRule =
IntentsTestRule( ActivityTestRule(
LoginActivity::class.java LoginActivity::class.java
) )
@ -44,8 +64,87 @@ class IntentTest {
.targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE) .targetContext.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE)
preferences.edit().putString("accessToken", "azerty").apply() preferences.edit().putString("accessToken", "azerty").apply()
preferences.edit().putString("domain", baseUrl.toString()).apply() preferences.edit().putString("domain", baseUrl.toString()).apply()
Intents.init()
} }
@Test
fun clickingMentionOpensProfile() {
ActivityScenario.launch(MainActivity::class.java)
val account = Account("1450", "deerbard_photo", "deerbard_photo",
"https://pixelfed.social/deerbard_photo", "deerbard photography",
"",
"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"", "", false, emptyList(), false,
"2018-08-01T12:58:21.000000Z", 72, 68, 27,
null, null, false, null)
val expectedIntent: Matcher<Intent> = CoreMatchers.allOf(
IntentMatchers.hasExtra(ACCOUNT_TAG, account)
)
Thread.sleep(1000)
//Click the mention
Espresso.onView(ViewMatchers.withId(R.id.list))
.perform(RecyclerViewActions.actionOnItemAtPosition<PostViewHolder>
(0, clickClickableSpanInDescription("@Dobios")))
//Wait a bit
Thread.sleep(1000)
//Check that the Profile is shown
intended(expectedIntent)
}
fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan";
}
override fun perform(uiController: UiController, view: View) {
val textView = view.findViewById<View>(R.id.description) as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView)
return;
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
@Test @Test
fun launchesIntent() { fun launchesIntent() {
@ -63,7 +162,10 @@ class IntentTest {
Thread.sleep(1000) Thread.sleep(1000)
intended(expectedIntent) intended(expectedIntent)
}
@After
fun after() {
Intents.release()
} }
} }

View File

@ -5,10 +5,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import android.net.Uri import android.net.Uri
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString
@ -24,6 +27,7 @@ import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -52,14 +56,14 @@ class LoginInstrumentedTest {
@Test @Test
fun invalidURL() { fun invalidURL() {
onView(withId(R.id.editText)).perform(ViewActions.replaceText("/jdi"), ViewActions.closeSoftKeyboard()) onView(withId(R.id.editText)).perform(ViewActions.replaceText("/jdi"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click()) onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain"))) onView(withId(R.id.editText)).check(matches(hasErrorText("Invalid domain")))
} }
@Test @Test
fun notPixelfedInstance() { fun notPixelfedInstance() {
onView(withId(R.id.editText)).perform(ViewActions.replaceText("localhost"), ViewActions.closeSoftKeyboard()) onView(withId(R.id.editText)).perform(ViewActions.replaceText("localhost"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click()) onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
onView(withId(R.id.editText)).check(matches(hasErrorText("Could not register the application with this server"))) onView(withId(R.id.editText)).check(matches(hasErrorText("Could not register the application with this server")))
} }
} }
@ -69,37 +73,50 @@ class LoginCheckIntent {
@get:Rule @get:Rule
var globalTimeout: Timeout = Timeout.seconds(100) var globalTimeout: Timeout = Timeout.seconds(100)
@get:Rule @get:Rule
val intentsTestRule = IntentsTestRule(LoginActivity::class.java) val intentsTestRule = ActivityTestRule(LoginActivity::class.java)
@Before
fun before() {
Intents.init()
}
@Test @Test
fun launchesOAuthIntent() { fun launchesOAuthIntent() {
ActivityScenario.launch(LoginActivity::class.java)
val expectedIntent: Matcher<Intent> = allOf( val expectedIntent: Matcher<Intent> = allOf(
hasAction(ACTION_VIEW), hasAction(ACTION_VIEW),
hasDataString(containsString("pixelfed.social")) hasDataString(containsString("pixelfed.social"))
) )
Thread.sleep(1000)
onView(withId(R.id.editText)).perform(ViewActions.replaceText("pixelfed.social"), ViewActions.closeSoftKeyboard()) onView(withId(R.id.editText)).perform(scrollTo()).perform(ViewActions.replaceText("pixelfed.social"), ViewActions.closeSoftKeyboard())
onView(withId(R.id.connect_instance_button)).perform(click()) onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click())
Thread.sleep(5000) Thread.sleep(3000)
intended(expectedIntent) intended(expectedIntent)
} }
@Test @Test
fun launchesInstanceInfo() { fun launchesInstanceInfo() {
ActivityScenario.launch(LoginActivity::class.java)
val expectedIntent: Matcher<Intent> = allOf( val expectedIntent: Matcher<Intent> = allOf(
hasAction(ACTION_VIEW), hasAction(ACTION_VIEW),
hasDataString(containsString("pixelfed.org/join")) hasDataString(containsString("pixelfed.org/join"))
) )
onView(withId(R.id.whatsAnInstanceTextView)).perform(click()) onView(withId(R.id.whatsAnInstanceTextView)).perform(scrollTo()).perform(click())
Thread.sleep(1000) Thread.sleep(10000)
intended(expectedIntent) intended(expectedIntent)
} }
@After
fun after() {
Intents.release()
}
} }
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)

View File

@ -1,29 +1,23 @@
package com.h.pixeldroid package com.h.pixeldroid
import android.content.Context import android.content.Context
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.*
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.DrawerMatchers
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.h.pixeldroid.fragments.feeds.ViewHolder import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.slowSwipeUp
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.typeTextInViewWithId
import com.h.pixeldroid.testUtility.MockServer import com.h.pixeldroid.testUtility.MockServer
import org.hamcrest.BaseMatcher
import org.hamcrest.Matcher
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -33,84 +27,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MockedServerTest { class MockedServerTest {
private fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("first matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
}
return false
}
}
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way up
* 0: swipes half way up
*/
private fun slowSwipeUp(percent: Boolean) : ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.SLOW,
GeneralLocation.BOTTOM_CENTER,
if(percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER,
Press.FINGER)
)
}
fun getText(matcher: Matcher<View?>?): String? {
val stringHolder = arrayOf<String?>(null)
onView(matcher).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isAssignableFrom(TextView::class.java)
}
override fun getDescription(): String {
return "getting text from a TextView"
}
override fun perform(
uiController: UiController,
view: View
) {
val tv = view as TextView //Save, because of check in getConstraints()
stringHolder[0] = tv.text.toString()
}
})
return stringHolder[0]
}
private fun clickChildViewWithId(id: Int) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
}
private fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<EditText>(id)
v.text.append(text)
}
}
private val mockServer = MockServer() private val mockServer = MockServer()
@ -234,22 +150,43 @@ class MockedServerTest {
Thread.sleep(1000) Thread.sleep(1000)
//Get initial like count //Get initial like count
val likes = getText(withId(R.id.nlikes)) val likes = getText(first(withId(R.id.nlikes)))
//Like the post //Like the post
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.liker))) (0, clickChildViewWithId(R.id.liker)))
Thread.sleep(100) Thread.sleep(100)
//Unlike the post //Unlike the post
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.liker))) (0, clickChildViewWithId(R.id.liker)))
//... //...
Thread.sleep(100) Thread.sleep(100)
//Profit //Profit
onView(withId(R.id.nlikes)).check(matches((withText(likes)))) onView(first(withId(R.id.nlikes))).check(matches((withText(likes))))
}
@Test
fun clickingLikeButtonFails() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Get initial like count
val likes = getText(first(withId(R.id.nlikes)))
//Like the post
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(2, clickChildViewWithId(R.id.liker)))
Thread.sleep(100)
//...
Thread.sleep(100)
//Profit
onView((withId(R.id.list))).check(matches(isDisplayed()))
} }
@Test @Test
@ -259,7 +196,7 @@ class MockedServerTest {
//Get initial like count //Get initial like count
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.username))) (0, clickChildViewWithId(R.id.username)))
Thread.sleep(1000) Thread.sleep(1000)
@ -275,7 +212,7 @@ class MockedServerTest {
//Get initial like count //Get initial like count
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.profilePic))) (0, clickChildViewWithId(R.id.profilePic)))
Thread.sleep(1000) Thread.sleep(1000)
@ -284,16 +221,86 @@ class MockedServerTest {
onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed())) onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed()))
} }
@Test
fun clickingReblogButtonWorks() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Get initial like count
val shares = getText(first(withId(R.id.nshares)))
//Reblog the post
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
Thread.sleep(100)
//UnReblog the post
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.reblogger)))
//...
Thread.sleep(100)
//Profit
onView(first(withId(R.id.nshares))).check(matches((withText(shares))))
}
@Test
fun clickingMentionOpensProfile() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Click the mention
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.description)))
//Wait a bit
Thread.sleep(1000)
//Check that the Profile is shown
onView(first(withId(R.id.username))).check(matches(isDisplayed()))
}
@Test
fun clickingHashTagsWorks() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Click the hashtag
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(1, clickChildViewWithId(R.id.description)))
//Wait a bit
Thread.sleep(1000)
//Check that the HashTag was indeed clicked
//Doesn't do anything for now
onView(withId(R.id.list)).check(matches(isDisplayed()))
}
@Test @Test
fun clickingCommentButtonOpensCommentSection() { fun clickingCommentButtonOpensCommentSection() {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000) Thread.sleep(1000)
//Click comment button and then try to see if the commenter exists
//Click comment button 3 times and then try to see if the commenter exists
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.commenter))) (0, clickChildViewWithId(R.id.commenter)))
Thread.sleep(1000) Thread.sleep(100)
onView(withId(R.id.commentIn)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.commenter)))
Thread.sleep(100)
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.commenter)))
onView(first(withId(R.id.commentIn)))
.check(matches(hasDescendant(withId(R.id.editComment)))) .check(matches(hasDescendant(withId(R.id.editComment))))
} }
@ -303,13 +310,25 @@ class MockedServerTest {
Thread.sleep(1000) Thread.sleep(1000)
//Open the comment section //Open the comment section
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.ViewComments))) (0, clickChildViewWithId(R.id.ViewComments)))
Thread.sleep(1000) Thread.sleep(1000)
onView(withId(R.id.commentContainer)) onView(first(withId(R.id.commentContainer)))
.check(matches(hasDescendant(withId(R.id.comment)))) .check(matches(hasDescendant(withId(R.id.comment))))
} }
@Test
fun clickingViewCommentFails() {
ActivityScenario.launch(MainActivity::class.java)
Thread.sleep(1000)
//Open the comment section
onView(withId(R.id.list))
.perform(actionOnItemAtPosition<PostViewHolder>
(2, clickChildViewWithId(R.id.ViewComments)))
Thread.sleep(1000)
onView(withId(R.id.list)).check(matches(isDisplayed()))
}
@Test @Test
fun postingACommentWorks() { fun postingACommentWorks() {
ActivityScenario.launch(MainActivity::class.java) ActivityScenario.launch(MainActivity::class.java)
@ -317,23 +336,21 @@ class MockedServerTest {
//Open the comment section //Open the comment section
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.commenter))) (0, clickChildViewWithId(R.id.commenter)))
onView(withId(R.id.list)).perform(slowSwipeUp(true))
onView(withId(R.id.list)).perform(slowSwipeUp(false))
onView(withId(R.id.list)).perform(slowSwipeUp(false)) onView(withId(R.id.list)).perform(slowSwipeUp(false))
Thread.sleep(1000) Thread.sleep(1000)
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, typeTextInViewWithId(R.id.editComment, "test"))) (0, typeTextInViewWithId(R.id.editComment, "test")))
onView(withId(R.id.list)) onView(withId(R.id.list))
.perform(actionOnItemAtPosition<ViewHolder> .perform(actionOnItemAtPosition<PostViewHolder>
(0, clickChildViewWithId(R.id.submitComment))) (0, clickChildViewWithId(R.id.submitComment)))
Thread.sleep(1000) Thread.sleep(1000)
onView(withId(R.id.commentContainer)) onView(first(withId(R.id.commentContainer)))
.check(matches(hasDescendant(withId(R.id.comment)))) .check(matches(hasDescendant(withId(R.id.comment))))
} }
} }

View File

@ -0,0 +1,95 @@
package com.h.pixeldroid.testUtility
import android.view.View
import android.widget.EditText
import android.widget.TextView
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.*
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.BaseMatcher
import org.hamcrest.Matcher
abstract class CustomMatchers {
companion object {
fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() {
var isFirst = true
override fun describeTo(description: org.hamcrest.Description?) {
description?.appendText("first matching item")
}
override fun matches(item: Any?): Boolean {
if (isFirst && matcher.matches(item)) {
isFirst = false
return true
}
return false
}
}
}
/**
* @param percent can be 1 or 0
* 1: swipes all the way up
* 0: swipes half way up
*/
fun slowSwipeUp(percent: Boolean) : ViewAction {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.SLOW,
GeneralLocation.BOTTOM_CENTER,
if(percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER,
Press.FINGER)
)
}
fun getText(matcher: Matcher<View?>?): String? {
val stringHolder = arrayOf<String?>(null)
Espresso.onView(matcher).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(TextView::class.java)
}
override fun getDescription(): String {
return "getting text from a TextView"
}
override fun perform(
uiController: UiController,
view: View
) {
val tv = view as TextView //Save, because of check in getConstraints()
stringHolder[0] = tv.text.toString()
}
})
return stringHolder[0]
}
fun clickChildViewWithId(id: Int) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<View>(id)
v.performClick()
}
}
fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction {
override fun getConstraints() = null
override fun getDescription() = "click child view with id $id"
override fun perform(uiController: UiController, view: View) {
val v = view.findViewById<EditText>(id)
v.text.append(text)
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -77,6 +77,20 @@ interface PixelfedAPI {
@Field("language") language : String? = null @Field("language") language : String? = null
) : Call<Status> ) : Call<Status>
@FormUrlEncoded
@POST("/api/v1/statuses/{id}/reblog")
fun reblogStatus(
@Header("Authorization") authorization: String,
@Path("id") statusId: String,
@Field("visibility") visibility: String? = null
) : Call<Status>
@POST("/api/v1/statuses/{id}/unreblog")
fun undoReblogStatus(
@Path("id") statusId: String,
@Header("Authorization") authorization: String
) : Call<Status>
//Used in our case to retrieve comments for a given status //Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context") @GET("/api/v1/statuses/{id}/context")
fun statusComments( fun statusComments(
@ -133,6 +147,12 @@ interface PixelfedAPI {
@Path("id") account_id: String? = null @Path("id") account_id: String? = null
): Call<List<Status>> ): Call<List<Status>>
@GET("/api/v1/accounts/{id}")
fun getAccount(
@Header("Authorization") authorization: String,
@Path("id") accountId : String
): Call<Account>
companion object { companion object {
fun create(baseUrl: String): PixelfedAPI { fun create(baseUrl: String): PixelfedAPI {
return Retrofit.Builder() return Retrofit.Builder()

View File

@ -12,9 +12,8 @@ import com.bumptech.glide.Glide
import com.h.pixeldroid.BuildConfig import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.ViewHolder import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.objects.Status import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.objects.Status.Companion.POST_TAG import com.h.pixeldroid.objects.Status.Companion.POST_TAG
import kotlinx.android.synthetic.main.post_fragment.view.* import kotlinx.android.synthetic.main.post_fragment.view.*
@ -34,15 +33,19 @@ class PostFragment : Fragment() {
status?.setupPost(root, picRequest, root.postPicture, root.profilePic) status?.setupPost(root, picRequest, root.postPicture, root.profilePic)
//Setup arguments needed for the onclicklisteners //Setup arguments needed for the onclicklisteners
val holder = ViewHolder(root, requireContext()) val holder = PostViewHolder(root, requireContext())
val preferences = requireActivity().getSharedPreferences( val preferences = requireActivity().getSharedPreferences(
"${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE "${BuildConfig.APPLICATION_ID}.pref", Context.MODE_PRIVATE
) )
val accessToken = preferences.getString("accessToken", "") val accessToken = preferences.getString("accessToken", "")
val api = PixelfedAPI.create("${preferences.getString("domain", "")}") val api = PixelfedAPI.create("${preferences.getString("domain", "")}")
status?.setDescription(root, api, "Bearer $accessToken")
//Activate onclickListeners //Activate onclickListeners
status?.activateLiker(holder, api, "Bearer $accessToken") status?.activateLiker(holder, api, "Bearer $accessToken", status.favourited)
status?.activateReblogger(holder, api, "Bearer $accessToken", status.reblogged)
status?.activateCommenter(holder, api, "Bearer $accessToken") status?.activateCommenter(holder, api, "Bearer $accessToken")
status?.showComments(holder, api, "Bearer $accessToken") status?.showComments(holder, api, "Bearer $accessToken")

View File

@ -13,6 +13,7 @@ import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.SparkButton
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.ListPreloader import com.bumptech.glide.ListPreloader
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
@ -23,7 +24,7 @@ import com.h.pixeldroid.objects.Status
import retrofit2.Call import retrofit2.Call
class HomeFragment : FeedFragment<Status, ViewHolder>() { class HomeFragment : FeedFragment<Status, PostViewHolder>() {
lateinit var picRequest: RequestBuilder<Drawable> lateinit var picRequest: RequestBuilder<Drawable>
@ -81,21 +82,21 @@ class HomeFragment : FeedFragment<Status, ViewHolder>() {
/** /**
* [RecyclerView.Adapter] that can display a list of Statuses * [RecyclerView.Adapter] that can display a list of Statuses
*/ */
inner class HomeRecyclerViewAdapter() inner class HomeRecyclerViewAdapter
: FeedsRecyclerViewAdapter<Status, ViewHolder>() { : FeedsRecyclerViewAdapter<Status, PostViewHolder>() {
private val api = pixelfedAPI private val api = pixelfedAPI
private val credential = "Bearer $accessToken" private val credential = "Bearer $accessToken"
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.post_fragment, parent, false) .inflate(R.layout.post_fragment, parent, false)
context = view.context context = view.context
return ViewHolder(view, context) return PostViewHolder(view, context)
} }
/** /**
* Binds the different elements of the Post Model to the view holder * Binds the different elements of the Post Model to the view holder
*/ */
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val post = getItem(position) ?: return val post = getItem(position) ?: return
val metrics = context.resources.displayMetrics val metrics = context.resources.displayMetrics
//Limit the height of the different images //Limit the height of the different images
@ -105,17 +106,20 @@ class HomeFragment : FeedFragment<Status, ViewHolder>() {
//Setup the post layout //Setup the post layout
post.setupPost(holder.postView, picRequest, holder.postPic, holder.profilePic) post.setupPost(holder.postView, picRequest, holder.postPic, holder.profilePic)
//Set initial favorite toggle value //Set the special HTML text
holder.isLiked = post.favourited post.setDescription(holder.postView, api, credential)
//Activate liker //Activate liker
post.activateLiker(holder, api, credential) post.activateLiker(holder, api, credential, post.favourited)
//Show comments //Show comments
post.showComments(holder, api, credential) post.showComments(holder, api, credential)
//Activate Commenter //Activate Commenter
post.activateCommenter(holder, api, credential) post.activateCommenter(holder, api, credential)
//Activate Reblogger
post.activateReblogger(holder, api ,credential, post.reblogged)
} }
override fun getPreloadItems(position: Int): MutableList<Status> { override fun getPreloadItems(position: Int): MutableList<Status> {
@ -132,7 +136,7 @@ class HomeFragment : FeedFragment<Status, ViewHolder>() {
/** /**
* Represents the posts that will be contained within the feed * Represents the posts that will be contained within the feed
*/ */
class ViewHolder(val postView: View, val context: android.content.Context) : RecyclerView.ViewHolder(postView) { class PostViewHolder(val postView: View, val context: android.content.Context) : RecyclerView.ViewHolder(postView) {
val profilePic : ImageView = postView.findViewById(R.id.profilePic) val profilePic : ImageView = postView.findViewById(R.id.profilePic)
val postPic : ImageView = postView.findViewById(R.id.postPicture) val postPic : ImageView = postView.findViewById(R.id.postPicture)
val username : TextView = postView.findViewById(R.id.username) val username : TextView = postView.findViewById(R.id.username)
@ -140,12 +144,15 @@ class ViewHolder(val postView: View, val context: android.content.Context) : Rec
val description : TextView = postView.findViewById(R.id.description) val description : TextView = postView.findViewById(R.id.description)
val nlikes : TextView = postView.findViewById(R.id.nlikes) val nlikes : TextView = postView.findViewById(R.id.nlikes)
val nshares : TextView = postView.findViewById(R.id.nshares) val nshares : TextView = postView.findViewById(R.id.nshares)
val liker : ImageView = postView.findViewById(R.id.liker)
//Spark buttons
val liker : SparkButton = postView.findViewById(R.id.liker)
val reblogger : SparkButton = postView.findViewById(R.id.reblogger)
val submitCmnt : ImageButton = postView.findViewById(R.id.submitComment) val submitCmnt : ImageButton = postView.findViewById(R.id.submitComment)
val commenter : ImageView = postView.findViewById(R.id.commenter) val commenter : ImageView = postView.findViewById(R.id.commenter)
val comment : EditText = postView.findViewById(R.id.editComment) val comment : EditText = postView.findViewById(R.id.editComment)
val commentCont : LinearLayout = postView.findViewById(R.id.commentContainer) val commentCont : LinearLayout = postView.findViewById(R.id.commentContainer)
val commentIn : LinearLayout = postView.findViewById(R.id.commentIn) val commentIn : LinearLayout = postView.findViewById(R.id.commentIn)
val viewComment : TextView = postView.findViewById(R.id.ViewComments) val viewComment : TextView = postView.findViewById(R.id.ViewComments)
var isLiked : Boolean = false
} }

View File

@ -27,6 +27,7 @@ import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Account import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Notification import com.h.pixeldroid.objects.Notification
import com.h.pixeldroid.objects.Status import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
import kotlinx.android.synthetic.main.fragment_notifications.view.* import kotlinx.android.synthetic.main.fragment_notifications.view.*
import retrofit2.Call import retrofit2.Call
@ -145,7 +146,15 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.N
setNotificationType(notification.type, notification.account.username, holder.notificationType) setNotificationType(notification.type, notification.account.username, holder.notificationType)
holder.postDescription.text = notification.status?.content ?: "" //Convert HTML to clickable text
holder.postDescription.text =
parseHTMLText(
notification.status?.content ?: "",
notification.status?.mentions,
pixelfedAPI,
context,
"Bearer $accessToken"
)
with(holder.mView) { with(holder.mView) {
@ -167,11 +176,11 @@ class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.N
} }
Notification.NotificationType.reblog -> { Notification.NotificationType.reblog -> {
setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_share) setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_reblog_blue)
} }
Notification.NotificationType.favourite -> { Notification.NotificationType.favourite -> {
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_heart) setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_like_full)
} }
} }
textView.text = format.format(username) textView.text = format.format(username)

View File

@ -3,13 +3,18 @@ package com.h.pixeldroid.objects
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.util.Log
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import com.h.pixeldroid.ProfileActivity import com.h.pixeldroid.ProfileActivity
import com.h.pixeldroid.R import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.utils.ImageConverter import com.h.pixeldroid.utils.ImageConverter
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.Serializable import java.io.Serializable
/* /*
@ -18,65 +23,92 @@ https://docs.joinmastodon.org/entities/account/
*/ */
data class Account( data class Account(
//Base attributes //Base attributes
val id: String, val id: String,
val username: String, val username: String,
val acct: String, val acct: String,
val url: String, //HTTPS URL val url: String, //HTTPS URL
//Display attributes //Display attributes
val display_name: String, val display_name: String,
val note: String, //HTML val note: String, //HTML
val avatar: String, //URL val avatar: String, //URL
val avatar_static: String, //URL val avatar_static: String, //URL
val header: String, //URL val header: String, //URL
val header_static: String, //URL val header_static: String, //URL
val locked: Boolean, val locked: Boolean,
val emojis: List<Emoji>, val emojis: List<Emoji>,
val discoverable: Boolean, val discoverable: Boolean,
//Statistical attributes //Statistical attributes
val created_at: String, //ISO 8601 Datetime (maybe can use a date type) val created_at: String, //ISO 8601 Datetime (maybe can use a date type)
val statuses_count: Int, val statuses_count: Int,
val followers_count: Int, val followers_count: Int,
val following_count: Int, val following_count: Int,
//Optional attributes //Optional attributes
val moved: Account? = null, val moved: Account? = null,
val fields: List<Field>? = emptyList(), val fields: List<Field>? = emptyList(),
val bot: Boolean = false, val bot: Boolean = false,
val source: Source? = null val source: Source? = null
) : Serializable { ) : Serializable {
companion object { companion object {
const val ACCOUNT_TAG = "AccountTag" const val ACCOUNT_TAG = "AccountTag"
/**
* @brief Opens an activity of the profile withn the given id
*/
fun getAccountFromId(id: String, api : PixelfedAPI, context: Context, credential: String) {
Log.e("ACCOUNT_ID", id)
api.getAccount(credential, id).enqueue( object : Callback<Account> {
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("GET ACCOUNT ERROR", t.toString())
}
override fun onResponse(
call: Call<Account>,
response: Response<Account>
) {
if(response.code() == 200) {
val account = response.body()!!
//Open the account page in a seperate activity
account.openProfile(context)
} else {
Log.e("ERROR CODE", response.code().toString())
}
}
})
} }
}
// Open profile activity with given account // Open profile activity with given account
fun openProfile(context: Context) { fun openProfile(context: Context) {
val intent = Intent(context, ProfileActivity::class.java) val intent = Intent(context, ProfileActivity::class.java)
intent.putExtra(Account.ACCOUNT_TAG, this) intent.putExtra(Account.ACCOUNT_TAG, this)
startActivity(context, intent, null) startActivity(context, intent, null)
} }
// Populate myProfile page with user's data // Populate myProfile page with user's data
fun setContent(view: View) { fun setContent(view: View) {
val profilePicture = view.findViewById<ImageView>(R.id.profilePictureImageView) val profilePicture = view.findViewById<ImageView>(R.id.profilePictureImageView)
ImageConverter.setRoundImageFromURL(view, this.avatar, profilePicture) ImageConverter.setRoundImageFromURL(view, this.avatar, profilePicture)
val description = view.findViewById<TextView>(R.id.descriptionTextView) val description = view.findViewById<TextView>(R.id.descriptionTextView)
description.text = this.note description.text = this.note
val accountName = view.findViewById<TextView>(R.id.accountNameTextView) val accountName = view.findViewById<TextView>(R.id.accountNameTextView)
accountName.text = this.username accountName.text = this.username
accountName.setTypeface(null, Typeface.BOLD) accountName.setTypeface(null, Typeface.BOLD)
val nbPosts = view.findViewById<TextView>(R.id.nbPostsTextView) val nbPosts = view.findViewById<TextView>(R.id.nbPostsTextView)
nbPosts.text = "${this.statuses_count}\nPosts" nbPosts.text = "${this.statuses_count}\nPosts"
nbPosts.setTypeface(null, Typeface.BOLD) nbPosts.setTypeface(null, Typeface.BOLD)
val nbFollowers = view.findViewById<TextView>(R.id.nbFollowersTextView) val nbFollowers = view.findViewById<TextView>(R.id.nbFollowersTextView)
nbFollowers.text = "${this.followers_count}\nFollowers" nbFollowers.text = "${this.followers_count}\nFollowers"
nbFollowers.setTypeface(null, Typeface.BOLD) nbFollowers.setTypeface(null, Typeface.BOLD)
val nbFollowing = view.findViewById<TextView>(R.id.nbFollowingTextView) val nbFollowing = view.findViewById<TextView>(R.id.nbFollowingTextView)
nbFollowing.text = "${this.following_count}\nFollowing" nbFollowing.text = "${this.following_count}\nFollowing"
nbFollowing.setTypeface(null, Typeface.BOLD) nbFollowing.setTypeface(null, Typeface.BOLD)
} }
} }

View File

@ -2,5 +2,10 @@ package com.h.pixeldroid.objects
import java.io.Serializable import java.io.Serializable
class Mention : Serializable { data class Mention(
} //Mentioned user
val id: String,
val username : String,
val acct : String, //URI of mentioned user (username if local, else username@domain)
val url : String //URL of mentioned user's profile
) : Serializable

View File

@ -1,22 +1,32 @@
package com.h.pixeldroid.objects package com.h.pixeldroid.objects
import android.content.Context
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.text.toSpanned
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.h.pixeldroid.R import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.ViewHolder import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
import com.h.pixeldroid.utils.ImageConverter import com.h.pixeldroid.utils.ImageConverter
import com.h.pixeldroid.utils.PostUtils.Companion.likePostCall import com.h.pixeldroid.utils.PostUtils.Companion.likePostCall
import com.h.pixeldroid.utils.PostUtils.Companion.postComment import com.h.pixeldroid.utils.PostUtils.Companion.postComment
import com.h.pixeldroid.utils.PostUtils.Companion.reblogPost
import com.h.pixeldroid.utils.PostUtils.Companion.retrieveComments import com.h.pixeldroid.utils.PostUtils.Companion.retrieveComments
import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput
import com.h.pixeldroid.utils.PostUtils.Companion.unLikePostCall import com.h.pixeldroid.utils.PostUtils.Companion.unLikePostCall
import com.h.pixeldroid.utils.PostUtils.Companion.undoReblogPost
import java.io.Serializable import java.io.Serializable
/* /*
@ -70,17 +80,21 @@ data class Status(
fun getProfilePicUrl() : String? = account.avatar fun getProfilePicUrl() : String? = account.avatar
fun getPostPreviewURL() : String? = media_attachments?.getOrNull(0)?.preview_url fun getPostPreviewURL() : String? = media_attachments?.getOrNull(0)?.preview_url
fun getDescription() : CharSequence { /**
val description = content as CharSequence * @brief returns the parsed version of the HTML description
*/
private fun getDescription(api: PixelfedAPI, context: Context, credential: String) : Spanned {
val description = content
if(description.isEmpty()) { if(description.isEmpty()) {
return "No description" return "No description".toSpanned()
} }
return description return parseHTMLText(description, mentions, api, context, credential)
} }
fun getUsername() : CharSequence { fun getUsername() : CharSequence {
var name = account?.display_name var name = account?.display_name
if (name.isNullOrEmpty()) { if (name.isEmpty()) {
name = account?.username name = account?.username
} }
return name!! return name!!
@ -112,8 +126,6 @@ data class Status(
usernameDesc.text = this.getUsername() usernameDesc.text = this.getUsername()
usernameDesc.setTypeface(null, Typeface.BOLD) usernameDesc.setTypeface(null, Typeface.BOLD)
rootView.findViewById<TextView>(R.id.description).text = this.getDescription()
val nlikes = rootView.findViewById<TextView>(R.id.nlikes) val nlikes = rootView.findViewById<TextView>(R.id.nlikes)
nlikes.text = this.getNLikes() nlikes.text = this.getNLikes()
nlikes.setTypeface(null, Typeface.BOLD) nlikes.setTypeface(null, Typeface.BOLD)
@ -135,25 +147,60 @@ data class Status(
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE
} }
fun activateLiker( fun setDescription(rootView: View, api : PixelfedAPI, credential: String) {
holder : ViewHolder, val desc = rootView.findViewById<TextView>(R.id.description)
api: PixelfedAPI,
credential: String desc.text = this.getDescription(api, rootView.context, credential)
desc.movementMethod = LinkMovementMethod.getInstance()
}
fun activateReblogger(
holder : PostViewHolder,
api : PixelfedAPI,
credential: String,
isReblogged : Boolean
) { ) {
//Activate the liker //Set initial button state
holder.liker.setOnClickListener { holder.reblogger.isChecked = isReblogged
if (holder.isLiked) {
//Unlike the post //Activate the button
unLikePostCall(holder, api, credential, this) holder.reblogger.setEventListener { _, buttonState ->
if (buttonState) {
Log.e("REBLOG", "Reblogged post")
// Button is active
reblogPost(holder, api, credential, this)
} else { } else {
//like the post Log.e("REBLOG", "Undo Reblogged post")
likePostCall(holder, api, credential, this) // Button is inactive
undoReblogPost(holder, api, credential, this)
} }
} }
} }
fun activateLiker(
holder : PostViewHolder,
api: PixelfedAPI,
credential: String,
isLiked: Boolean
) {
//Set initial state
holder.liker.isChecked = isLiked
//Activate the liker
holder.liker.setEventListener { _, buttonState ->
if (buttonState) {
// Button is active
likePostCall(holder, api, credential, this)
} else {
// Button is inactive
unLikePostCall(holder, api, credential, this)
}
}
}
fun showComments( fun showComments(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String credential: String
) { ) {
@ -172,7 +219,7 @@ data class Status(
} }
fun activateCommenter( fun activateCommenter(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String credential: String
) { ) {

View File

@ -0,0 +1,126 @@
package com.h.pixeldroid.utils
import android.content.Context
import android.os.Build
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.core.text.toSpanned
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.Account.Companion.getAccountFromId
import com.h.pixeldroid.objects.Mention
import com.h.pixeldroid.utils.customSpans.ClickableSpanNoUnderline
import java.net.URI
import java.net.URISyntaxException
import java.util.Locale
class HtmlUtils {
companion object {
private fun fromHtml(html: String): Spanned {
val result: Spanned = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
} else {
Html.fromHtml(html)
}
return result.trim().toSpanned()
}
private fun getDomain(urlString: String?): String {
val uri: URI
try {
uri = URI(urlString!!)
} catch (e: URISyntaxException) {
return ""
}
val host: String = uri.host
return if (host.startsWith("www.")) {
host.substring(4)
} else {
host
}
}
fun parseHTMLText(
text : String,
mentions: List<Mention>?,
api : PixelfedAPI,
context: Context,
credential: String
) : Spanned {
//Convert text to spannable
val content = fromHtml(text)
//Retrive all links that should be made clickable
val builder = SpannableStringBuilder(content)
val urlSpans = content.getSpans(0, content.length, URLSpan::class.java)
for(span in urlSpans) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
val flags = builder.getSpanFlags(span)
val text = builder.subSequence(start, end)
var customSpan: ClickableSpan? = null
//Handle hashtags
if (text[0] == '#') {
val tag = text.subSequence(1, text.length).toString()
customSpan = object : ClickableSpanNoUnderline() {
override fun onClick(widget: View) {
Toast.makeText(context, tag, Toast.LENGTH_SHORT).show()
}
}
}
//Handle mentions
if(text[0] == '@' && !mentions.isNullOrEmpty()) {
val accountUsername = text.subSequence(1, text.length).toString()
var id: String? = null
//Go through all mentions stored in the status
for (mention in mentions) {
if (mention.username.toLowerCase(Locale.ROOT)
== accountUsername.toLowerCase(Locale.ROOT)
) {
id = mention.id
//Mentions can be of users in other domains
if (mention.url.contains(getDomain(span.url))) {
break
}
}
}
//Check that we found a user for the given mention
if (id != null) {
val accountId: String = id
customSpan = object : ClickableSpanNoUnderline() {
override fun onClick(widget: View) {
Log.e("MENTION", "CLICKED")
//Retrieve the account for the given profile
getAccountFromId(accountId, api, context, credential)
}
}
}
}
builder.removeSpan(span);
builder.setSpan(customSpan, start, end, flags);
// Add zero-width space after links in end of line to fix its too large hitbox.
if (end >= builder.length || builder.subSequence(end, end + 1).toString() == "\n") {
builder.insert(end, "\u200B")
}
}
return builder
}
}
}

View File

@ -1,49 +1,110 @@
package com.h.pixeldroid.utils package com.h.pixeldroid.utils
import android.graphics.Typeface
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView
import com.h.pixeldroid.R import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.fragments.feeds.ViewHolder import com.h.pixeldroid.fragments.feeds.PostViewHolder
import com.h.pixeldroid.objects.Account
import com.h.pixeldroid.objects.Context import com.h.pixeldroid.objects.Context
import com.h.pixeldroid.objects.Status import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.ImageConverter.Companion.setImageFromDrawable
import kotlinx.android.synthetic.main.comment.view.* import kotlinx.android.synthetic.main.comment.view.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
class PostUtils { abstract class PostUtils {
companion object { companion object {
fun toggleCommentInput( fun toggleCommentInput(
holder : ViewHolder holder : PostViewHolder
) { ) {
//Toggle comment button //Toggle comment button
holder.commenter.setOnClickListener { holder.commenter.setOnClickListener {
when(holder.commentIn.visibility) { when(holder.commentIn.visibility) {
View.VISIBLE -> holder.commentIn.visibility = View.GONE View.VISIBLE -> {
View.INVISIBLE -> holder.commentIn.visibility = View.VISIBLE holder.commentIn.visibility = View.GONE
View.GONE -> holder.commentIn.visibility = View.VISIBLE setImageFromDrawable(holder.postView, holder.commenter, R.drawable.ic_comment_empty)
}
View.GONE -> {
holder.commentIn.visibility = View.VISIBLE
setImageFromDrawable(holder.postView, holder.commenter, R.drawable.ic_comment_blue)
}
} }
} }
} }
fun likePostCall( fun reblogPost(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String, credential: String,
post : Status post : Status
) { ) {
//Call the api function
api.reblogStatus(credential, post.id).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("REBLOG ERROR", t.toString())
holder.reblogger.isChecked = false
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
//Update shown share count
holder.nshares.text = resp.getNShares()
holder.reblogger.isChecked = resp.reblogged
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.reblogger.isChecked = false
}
}
})
}
fun undoReblogPost(
holder : PostViewHolder,
api: PixelfedAPI,
credential: String,
post : Status
) {
//Call the api function
api.undoReblogStatus(credential, post.id).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("REBLOG ERROR", t.toString())
holder.reblogger.isChecked = true
}
override fun onResponse(call: Call<Status>, response: Response<Status>) {
if(response.code() == 200) {
val resp = response.body()!!
//Update shown share count
holder.nshares.text = resp.getNShares()
holder.reblogger.isChecked = resp.reblogged
} else {
Log.e("RESPONSE_CODE", response.code().toString())
holder.reblogger.isChecked = true
}
}
})
}
fun likePostCall(
holder : PostViewHolder,
api: PixelfedAPI,
credential: String,
post : Status
) {
//Call the api function
api.likePost(credential, post.id).enqueue(object : Callback<Status> { api.likePost(credential, post.id).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) { override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("LIKE ERROR", t.toString()) Log.e("LIKE ERROR", t.toString())
holder.liker.isChecked = false
} }
override fun onResponse(call: Call<Status>, response: Response<Status>) { override fun onResponse(call: Call<Status>, response: Response<Status>) {
@ -52,9 +113,10 @@ class PostUtils {
//Update shown like count and internal like toggle //Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes() holder.nlikes.text = resp.getNLikes()
holder.isLiked = resp.favourited holder.liker.isChecked = resp.favourited
} else { } else {
Log.e("RESPOSE_CODE", response.code().toString()) Log.e("RESPONSE_CODE", response.code().toString())
holder.liker.isChecked = false
} }
} }
@ -62,14 +124,16 @@ class PostUtils {
} }
fun unLikePostCall( fun unLikePostCall(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String, credential: String,
post : Status post : Status
) { ) {
//Call the api function
api.unlikePost(credential, post.id).enqueue(object : Callback<Status> { api.unlikePost(credential, post.id).enqueue(object : Callback<Status> {
override fun onFailure(call: Call<Status>, t: Throwable) { override fun onFailure(call: Call<Status>, t: Throwable) {
Log.e("UNLIKE ERROR", t.toString()) Log.e("UNLIKE ERROR", t.toString())
holder.liker.isChecked = true
} }
override fun onResponse(call: Call<Status>, response: Response<Status>) { override fun onResponse(call: Call<Status>, response: Response<Status>) {
@ -78,9 +142,10 @@ class PostUtils {
//Update shown like count and internal like toggle //Update shown like count and internal like toggle
holder.nlikes.text = resp.getNLikes() holder.nlikes.text = resp.getNLikes()
holder.isLiked = resp.favourited holder.liker.isChecked = resp.favourited
} else { } else {
Log.e("RESPOSE_CODE", response.code().toString()) Log.e("RESPONSE_CODE", response.code().toString())
holder.liker.isChecked = true
} }
} }
@ -89,7 +154,7 @@ class PostUtils {
} }
fun postComment( fun postComment(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String, credential: String,
post : Status post : Status
@ -131,7 +196,7 @@ class PostUtils {
} }
fun retrieveComments( fun retrieveComments(
holder : ViewHolder, holder : PostViewHolder,
api: PixelfedAPI, api: PixelfedAPI,
credential: String, credential: String,
post : Status post : Status

View File

@ -0,0 +1,11 @@
package com.h.pixeldroid.utils.customSpans
import android.text.TextPaint
import android.text.style.ClickableSpan
abstract class ClickableSpanNoUnderline : ClickableSpan() {
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#1737A2"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,2H4c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2H4c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="21.6dp" android:viewportHeight="99"
android:viewportWidth="110" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M54.5,94.5C58.2,90.74 79.39,69.25 92.63,55.83C104.5,43.8 104.24,26.26 93.46,15.39C82.67,4.5 65.23,4.55 54.5,15.48C43.76,4.55 26.32,4.5 15.54,15.38C4.75,26.25 4.5,43.79 16.36,55.83C29.6,69.25 50.79,90.74 54.5,94.5Z"
android:strokeColor="#000000" android:strokeWidth="9"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="21.6dp" android:viewportHeight="99"
android:viewportWidth="110" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#c42f0a"
android:pathData="M54.5,94.5C58.2,90.74 79.39,69.25 92.63,55.83C104.5,43.8 104.24,26.26 93.46,15.39C82.67,4.5 65.23,4.55 54.5,15.48C43.76,4.55 26.32,4.5 15.54,15.38C4.75,26.25 4.5,43.79 16.36,55.83C29.6,69.25 50.79,90.74 54.5,94.5Z" android:strokeColor="#00000000"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector android:height="24dp" android:viewportHeight="85"
android:viewportWidth="111" android:width="31.341177dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M30.5,64.5L90.5,64.5L90.5,39.81"
android:strokeColor="#000000" android:strokeWidth="9"/>
<path android:fillColor="#000000"
android:pathData="M90.5,25.56L100,44.56L90.5,39.81L81,44.56Z"
android:strokeColor="#000000" android:strokeWidth="9"/>
<path android:fillColor="#00000000"
android:pathData="M80.5,18.5L20.5,18.5L20.5,44.19"
android:strokeColor="#000000" android:strokeWidth="9"/>
<path android:fillColor="#000000"
android:pathData="M20.5,58.44L11,39.44L20.5,44.19L30,39.44Z"
android:strokeColor="#000000" android:strokeWidth="9"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector android:height="24dp" android:viewportHeight="85"
android:viewportWidth="111" android:width="31.341177dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M30.5,64.5L90.5,64.5L90.5,39.81"
android:strokeColor="#0000ff" android:strokeWidth="9"/>
<path android:fillColor="#0000ff"
android:pathData="M90.5,25.56L100,44.56L90.5,39.81L81,44.56Z"
android:strokeColor="#0000ff" android:strokeWidth="9"/>
<path android:fillColor="#00000000"
android:pathData="M80.5,18.5L20.5,18.5L20.5,44.19"
android:strokeColor="#0000ff" android:strokeWidth="9"/>
<path android:fillColor="#0000ff"
android:pathData="M20.5,58.44L11,39.44L20.5,44.19L30,39.44Z"
android:strokeColor="#0000ff" android:strokeWidth="9"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#1737A2"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@ -1,4 +0,0 @@
<vector android:height="19.2dp" android:viewportHeight="512"
android:viewportWidth="640" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M629.657,343.598L528.971,444.284c-9.373,9.372 -24.568,9.372 -33.941,0L394.343,343.598c-9.373,-9.373 -9.373,-24.569 0,-33.941l10.823,-10.823c9.562,-9.562 25.133,-9.34 34.419,0.492L480,342.118L480,160L292.451,160a24.005,24.005 0,0 1,-16.971 -7.029l-16,-16C244.361,121.851 255.069,96 276.451,96L520,96c13.255,0 24,10.745 24,24v222.118l40.416,-42.792c9.285,-9.831 24.856,-10.054 34.419,-0.492l10.823,10.823c9.372,9.372 9.372,24.569 -0.001,33.941zM364.519,359.029A23.999,23.999 0,0 0,347.548 352L160,352L160,169.881l40.416,42.792c9.286,9.831 24.856,10.054 34.419,0.491l10.822,-10.822c9.373,-9.373 9.373,-24.569 0,-33.941L144.971,67.716c-9.373,-9.373 -24.569,-9.373 -33.941,0L10.343,168.402c-9.373,9.373 -9.373,24.569 0,33.941l10.822,10.822c9.562,9.562 25.133,9.34 34.419,-0.491L96,169.881L96,392c0,13.255 10.745,24 24,24h243.549c21.382,0 32.09,-25.851 16.971,-40.971l-16.001,-16z"/>
</vector>

View File

@ -1,200 +1,195 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".fragments.PostFragment">
<ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
<androidx.cardview.widget.CardView xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="5dp" tools:context=".fragments.PostFragment">
android:layout_marginBottom="5dp" <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:layout_marginTop="5dp"
android:layout_height="wrap_content"> android:layout_marginBottom="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false">
<ImageView
android:id="@+id/profilePic"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:src="@drawable/ic_default_user"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
app:layout_constraintStart_toEndOf="@+id/profilePic"
app:layout_constraintTop_toTopOf="@+id/profilePic"
tools:text="Account" />
<ImageView
android:id="@+id/postPicture"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:layout_marginTop="10dp"
android:adjustViewBounds="true"
app:layout_constraintTop_toBottomOf="@+id/profilePic"
tools:src="@color/browser_actions_bg_grey"/>
<LinearLayout <ImageView
android:id="@+id/linearLayout3" android:id="@+id/commenter"
android:layout_width="30dp"
android:layout_height="30dp"
android:importantForAccessibility="no"
android:padding="4dp"
android:src="@drawable/ic_comment_empty"
app:layout_constraintBottom_toBottomOf="@+id/liker"
app:layout_constraintEnd_toStartOf="@id/reblogger"
app:layout_constraintStart_toEndOf="@id/liker"
app:layout_constraintTop_toTopOf="@id/liker"
/>
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/liker"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:clipToPadding="false"
android:importantForAccessibility="no"
android:padding="4dp"
sparkbutton:activeImage="@drawable/ic_like_full"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="@drawable/ic_like_empty"
sparkbutton:primaryColor="@color/heart_red"
sparkbutton:secondaryColor="@color/black"
app:layout_constraintEnd_toStartOf="@id/commenter"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="@id/profilePic"
app:layout_constraintTop_toBottomOf="@id/postPicture"/>
<at.connyduck.sparkbutton.SparkButton
android:id="@+id/reblogger"
android:layout_width="30dp"
android:layout_height="30dp"
android:clipToPadding="false"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@+id/commenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/commenter"
app:layout_constraintTop_toTopOf="@+id/commenter"
sparkbutton:activeImage="@drawable/ic_reblog_blue"
sparkbutton:iconSize="28dp"
sparkbutton:inactiveImage="@drawable/ic_reblog"
sparkbutton:primaryColor="@color/share_blue"
sparkbutton:secondaryColor="@color/black"/>
<TextView
android:id="@+id/nlikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="50"
app:layout_constraintEnd_toEndOf="@+id/liker"
app:layout_constraintStart_toStartOf="@+id/liker"
app:layout_constraintTop_toBottomOf="@+id/liker"
tools:text="2 Likes" />
<TextView
android:id="@+id/nshares"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="50"
android:gravity="right"
app:layout_constraintEnd_toEndOf="@+id/reblogger"
app:layout_constraintStart_toStartOf="@+id/reblogger"
app:layout_constraintTop_toBottomOf="@+id/reblogger"
tools:text="3 Shares" />
<TextView
android:id="@+id/usernameDesc"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
app:layout_constraintStart_toStartOf="@+id/profilePic"
app:layout_constraintTop_toBottomOf="@+id/nlikes"
tools:text="Account"/>
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hyphenationFrequency="full"
app:layout_constraintStart_toStartOf="@+id/usernameDesc"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/usernameDesc"
tools:text="This is a description, describing stuff.\nIt contains multiple lines, and that's okay. It's also got some really long lines, and we love it for it." />
<LinearLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/description"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="3">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Comment"
android:importantForAutofill="no"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:layout_weight="1"
android:contentDescription="Submit button"
android:src="@drawable/ic_send_blue" />
</LinearLayout>
<LinearLayout
android:id="@+id/commentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_editor_absoluteY="315dp"
app:layout_constraintTop_toBottomOf="@+id/commentIn">
<TextView
android:id="@+id/ViewComments"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_marginStart="10dp"
app:layout_constraintTop_toTopOf="parent"> android:layout_marginBottom="10dp"
tools:text="TextView"/>
<androidx.constraintlayout.widget.ConstraintLayout </LinearLayout>
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<ImageView </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/profilePic"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_default_user"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView </androidx.cardview.widget.CardView>
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePic"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.516"
tools:text="TextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout2"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/postPicture"
android:adjustViewBounds="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/LikeShareConstraint"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginTop="10dp"
>
<LinearLayout
android:layout_width="409dp"
android:layout_height="28dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/liker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:src="@drawable/ic_heart" />
<ImageView
android:id="@+id/commenter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:src="@drawable/ic_add_black_24dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/nlikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="10dp"
android:layout_weight="50"
tools:text="TextView" />
<TextView
android:id="@+id/nshares"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:layout_marginRight="30dp"
android:layout_marginBottom="10dp"
android:layout_weight="50"
android:gravity="right"
tools:text="TextView" />
</LinearLayout>
<TextView
android:id="@+id/usernameDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
tools:text="TextView" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginBottom="10dp"
tools:text="TextView" />
<LinearLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="3">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="Comment" />
</com.google.android.material.textfield.TextInputLayout>
<ImageButton
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:src="@drawable/ic_add_black_24dp"
android:layout_weight="1"
android:contentDescription="Submit button" />
</LinearLayout>
<LinearLayout
android:id="@+id/commentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/ViewComments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
tools:text="TextView">
</TextView>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</ScrollView>
</FrameLayout> </FrameLayout>

View File

@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="auth_scheme" translatable="false">oauth2redirect</string> <string name="auth_scheme" translatable="false">oauth2redirect</string>
<declare-styleable name="SparkButton">
<attr format="dimension|reference" name="iconSize"/>
<attr format="reference" name="activeImage"/>
<attr format="reference" name="inactiveImage"/>
<attr format="reference" name="primaryColor"/>
<attr format="reference" name="secondaryColor"/>
<attr format="float" name="animationSpeed"/>
</declare-styleable>
<color name="heart_red">#c42f0a</color>
<color name="share_blue">#0000ff</color>
<color name="black">#000000</color>
</resources> </resources>

View File

@ -32,18 +32,6 @@ class PostUnitTest {
@Test @Test
fun getProfilePicUrlReturnsAValidURL() = Assert.assertNotNull(status.getProfilePicUrl()) fun getProfilePicUrlReturnsAValidURL() = Assert.assertNotNull(status.getProfilePicUrl())
@Test
fun getDescriptionReturnsDefaultIfEmpty() {
val emptyDescStatus = status.copy(content = "")
Assert.assertEquals( "No description", emptyDescStatus.getDescription())
}
@Test
fun getDescriptionReturnsAValidDesc() = Assert.assertNotNull(status.getDescription())
@Test
fun getDescriptionReturnsACorrectDesc() = Assert.assertEquals(status.content, status.getDescription())
@Test @Test
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.display_name, status.getUsername()) fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.display_name, status.getUsername())

View File

@ -5,7 +5,6 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.2' classpath 'com.android.tools.build:gradle:3.6.2'
@ -20,7 +19,8 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://jitpack.io" }
} }
} }