├── docs ├── main.png ├── toot.png └── multiple-accounts.png ├── src ├── test │ ├── resources │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ ├── test.fxml │ │ └── log4j.properties │ └── kotlin │ │ └── com │ │ └── github │ │ └── wakingrufus │ │ └── mastodon │ │ ├── config │ │ └── AccountConfigTest.kt │ │ ├── ui │ │ ├── AccountFragmentTest.kt │ │ ├── NotificationFragmentTest.kt │ │ ├── TootEditorTest.kt │ │ ├── AccountChooserViewTest.kt │ │ ├── NotificationFeedViewTest.kt │ │ ├── OAuthViewTest.kt │ │ └── ParseTootContentKtTest.kt │ │ ├── IntegrationTest.kt │ │ ├── account │ │ └── CreateAccountStateKtTest.kt │ │ └── TornadoFxTest.kt └── main │ ├── resources │ ├── images │ │ └── avatar-default.png │ └── log4j.properties │ ├── deploy │ └── package │ │ └── linux │ │ └── mastodon-jfx.png │ └── kotlin │ └── com │ └── github │ └── wakingrufus │ └── mastodon │ ├── Main.kt │ ├── config │ ├── ConfigData.kt │ ├── ConfigurationHandler.kt │ ├── AccountConfig.kt │ └── FileConfigurationHandler.kt │ ├── data │ ├── StatusFeed.kt │ ├── OAuthModel.kt │ ├── NotificationFeed.kt │ └── AccountState.kt │ ├── client │ ├── BuildPublicClient.kt │ ├── BuildStreamingClient.kt │ ├── BuildTimelinesClient.kt │ ├── ParseUrl.kt │ ├── BuildNotificationsClient.kt │ ├── ClientBuilder.kt │ ├── OkHttpLogger.kt │ ├── RegisterApp.kt │ ├── GetAccessToken.kt │ └── OAuthActions.kt │ ├── account │ ├── Identity.kt │ ├── GetClientForAccount.kt │ ├── CreateAccountConfig.kt │ └── CreateAccountState.kt │ ├── toot │ ├── BoostToot.kt │ └── CreateToot.kt │ ├── MastodonApplication.kt │ └── ui │ ├── AccountChooserView.kt │ ├── NotificationFeedView.kt │ ├── AccountFragment.kt │ ├── TootEditor.kt │ ├── styles │ └── DefaultStyles.kt │ ├── ParseTootContent.kt │ ├── MainView.kt │ ├── OAuthView.kt │ ├── NotificationFragment.kt │ ├── SettingsView.kt │ └── StatusFeedsView.kt ├── .gitignore ├── LICENSE ├── README.md └── shippable.yml /docs/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakingrufus/mastodon-jfx/HEAD/docs/main.png -------------------------------------------------------------------------------- /docs/toot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakingrufus/mastodon-jfx/HEAD/docs/toot.png -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | gradle/ 4 | gradlew.bat 5 | gradlew 6 | .idea/ 7 | shippable/ 8 | *.iml -------------------------------------------------------------------------------- /docs/multiple-accounts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakingrufus/mastodon-jfx/HEAD/docs/multiple-accounts.png -------------------------------------------------------------------------------- /src/test/resources/test.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/images/avatar-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakingrufus/mastodon-jfx/HEAD/src/main/resources/images/avatar-default.png -------------------------------------------------------------------------------- /src/main/deploy/package/linux/mastodon-jfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wakingrufus/mastodon-jfx/HEAD/src/main/deploy/package/linux/mastodon-jfx.png -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/Main.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon 2 | 3 | import tornadofx.launch 4 | 5 | fun main(args: Array) = launch(args) -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/config/ConfigData.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.config 2 | 3 | import java.util.* 4 | 5 | data class ConfigData(val identities: Set = HashSet()) { 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/data/StatusFeed.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.data 2 | 3 | import com.sys1yagi.mastodon4j.api.entity.Status 4 | import javafx.collections.ObservableList 5 | 6 | data class StatusFeed(val name: String, val server: String, val statuses: ObservableList) -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/BuildPublicClient.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.method.Public 5 | 6 | fun buildPublicClient(client: MastodonClient): Public { 7 | return Public(client) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/BuildStreamingClient.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.method.Streaming 5 | 6 | fun buildStreamingClient(client: MastodonClient): Streaming { 7 | return Streaming(client) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/BuildTimelinesClient.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.method.Timelines 5 | 6 | fun buildTimelinesClient(client: MastodonClient) : Timelines{ 7 | return Timelines(client) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/ParseUrl.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import java.net.URL 4 | 5 | fun parseUrl(url: String) : String { 6 | try { 7 | val urlObject = URL(url) 8 | return urlObject.host 9 | } catch (e: Exception) { 10 | return url 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/config/ConfigurationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.config 2 | 3 | interface ConfigurationHandler { 4 | 5 | fun readFileConfig(): ConfigData 6 | fun saveConfig(configData: ConfigData) 7 | fun addAccountToConfig(configData: ConfigData, identity: AccountConfig): ConfigData 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/BuildNotificationsClient.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.method.Notifications 5 | 6 | fun buildNotificationsClient(client: MastodonClient): Notifications { 7 | return Notifications(client) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/data/OAuthModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.data 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.entity.auth.AppRegistration 5 | 6 | data class OAuthModel (val client: MastodonClient, val appRegistration: AppRegistration, val token: String? = null){ 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/config/AccountConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.config 2 | 3 | data class AccountConfig(val accessToken: String, 4 | val clientId: String, 5 | val clientSecret: String, 6 | val username: String, 7 | val server: String) { 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/account/Identity.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.account 2 | 3 | data class Identity(val username: String, val server: String) { 4 | companion object {} 5 | } 6 | 7 | fun Identity.Companion.fromFqn(fqn: String): Identity { 8 | return Identity(username = fqn.split("@")[1], server = fqn.split("@")[2]) 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/data/NotificationFeed.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.data 2 | 3 | import com.sys1yagi.mastodon4j.api.entity.Notification 4 | import com.sys1yagi.mastodon4j.api.entity.Status 5 | import javafx.collections.ObservableList 6 | 7 | data class NotificationFeed(val name: String, val server: String, val notifications: ObservableList) -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/account/GetClientForAccount.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.account 2 | 3 | import com.github.wakingrufus.mastodon.data.AccountState 4 | import com.sys1yagi.mastodon4j.MastodonClient 5 | 6 | 7 | fun GetClientForAccount(id: Long, accountStates: List): MastodonClient? { 8 | var found: MastodonClient? = null 9 | val findFirst = accountStates.stream().filter { accountState -> accountState.account.id == id }.findFirst() 10 | if (findFirst.isPresent) { 11 | found = findFirst.get().client 12 | } 13 | return found 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/toot/BoostToot.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.toot 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.method.Statuses 5 | 6 | fun boostToot(id: Long, 7 | client: MastodonClient, 8 | statusesClient: () -> Statuses = { Statuses(client = client) }) { 9 | statusesClient.invoke().postReblog(id).execute() 10 | } 11 | 12 | fun unboostToot(id: Long, 13 | client: MastodonClient, 14 | statusesClient: () -> Statuses = { Statuses(client = client) }) { 15 | statusesClient.invoke().postUnreblog(id).execute() 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/account/CreateAccountConfig.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.account 2 | 3 | import com.github.wakingrufus.mastodon.config.AccountConfig 4 | import com.sys1yagi.mastodon4j.api.method.Accounts 5 | 6 | fun createAccountConfig(accountClient: Accounts, accessToken: String, clientId: String, clientSecret: String, server: String): AccountConfig { 7 | val account = accountClient.getVerifyCredentials().execute() 8 | return AccountConfig( 9 | accessToken = accessToken, 10 | clientId = clientId, 11 | clientSecret = clientSecret, 12 | username = account.userName, 13 | server = server) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/toot/CreateToot.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.toot 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.entity.Status 5 | import com.sys1yagi.mastodon4j.api.method.Statuses 6 | 7 | fun createToot(client: MastodonClient, 8 | statusesClient: () -> Statuses = { Statuses(client = client) }, 9 | status: String, 10 | inReplyToId: Long?): Status { 11 | return statusesClient.invoke().postStatus( 12 | status = status, 13 | inReplyToId = inReplyToId, 14 | sensitive = false, 15 | spoilerText = null, 16 | mediaIds = null).execute() 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, STDOUT, file 2 | log4j.logger.deng=INFO 3 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 4 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.STDOUT.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n 6 | log4j.appender.STDOUT.threshold=info 7 | 8 | 9 | log4j.appender.file=org.apache.log4j.RollingFileAppender 10 | log4j.appender.file.File=build/logging.log 11 | log4j.appender.file.MaxFileSize=5MB 12 | log4j.appender.file.MaxBackupIndex=20 13 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 14 | log4j.appender.file.layout.ConversionPattern=%-5p [%d{dd.MM.yy HH:mm:ss}] %C{1} - %m [thread: %t]\n 15 | log4j.appender.file.threshold=info -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=DEBUG, STDOUT, file 2 | log4j.logger.deng=DEBUG 3 | log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender 4 | log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.STDOUT.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n 6 | log4j.appender.STDOUT.threshold=debug 7 | 8 | log4j.appender.file=org.apache.log4j.RollingFileAppender 9 | log4j.appender.file.File=build/logging.log 10 | log4j.appender.file.MaxFileSize=5MB 11 | log4j.appender.file.MaxBackupIndex=20 12 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 13 | log4j.appender.file.layout.ConversionPattern=%-5p [%d{dd.MM.yy HH:mm:ss}] %C{1} - %m [thread: %t]\n 14 | log4j.appender.file.threshold=debug -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/config/AccountConfigTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.config 2 | 3 | import mu.KLogging 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | 7 | 8 | class AccountConfigTest { 9 | companion object : KLogging() 10 | 11 | @Test 12 | fun testDataClass() { 13 | val config1 = AccountConfig(accessToken = "accessToken", clientId = "clientId", clientSecret = "clientSecret", server = "server", username = "username") 14 | val config2 = config1.copy() 15 | assertEquals("objects are equal", config1, config2) 16 | assertEquals("hashcode is equal", config1.hashCode(), config2.hashCode()) 17 | assertEquals("toString is equal", config1.toString(), config2.toString()) 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/MastodonApplication.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon 2 | 3 | import com.github.wakingrufus.mastodon.ui.MainView 4 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 5 | import javafx.scene.image.Image 6 | import javafx.scene.text.Text 7 | import javafx.stage.Stage 8 | import mu.KLogging 9 | import tornadofx.App 10 | 11 | class MastodonApplication : App(MainView::class, DefaultStyles::class) { 12 | companion object : KLogging() 13 | 14 | override fun start(stage: Stage) { 15 | super.start(stage) 16 | val rootEm = Math.rint(Text().layoutBounds.height) 17 | 18 | stage.width = rootEm * 80 19 | stage.height = rootEm * 60 20 | stage.icons.add(Image(this.javaClass.getResourceAsStream("/images/avatar-default.png"))) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/ClientBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.google.gson.Gson 4 | import com.sys1yagi.mastodon4j.MastodonClient 5 | import okhttp3.OkHttpClient 6 | 7 | fun createAccountClient(instance: String, accessToken: String): MastodonClient { 8 | val httpClientBuilder = OkHttpClient.Builder().addInterceptor(OkHttpLogger()) 9 | var builder = MastodonClient.Builder(instance, httpClientBuilder, Gson()) 10 | builder = builder.accessToken(accessToken).useStreamingApi() 11 | return builder.build() 12 | } 13 | 14 | fun createServerClient(instance: String): MastodonClient { 15 | val httpClientBuilder = OkHttpClient.Builder().addInterceptor(OkHttpLogger()) 16 | val builder = MastodonClient.Builder(instance, httpClientBuilder, Gson()) 17 | return builder.build() 18 | } 19 | -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/AccountFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.TornadoFxTest 4 | import com.sys1yagi.mastodon4j.api.entity.Account 5 | import mu.KLogging 6 | import org.junit.Test 7 | import org.testfx.api.FxAssert 8 | import org.testfx.matcher.base.NodeMatchers 9 | 10 | 11 | class AccountFragmentTest : TornadoFxTest() { 12 | companion object : KLogging() 13 | 14 | @Test 15 | fun test() { 16 | showViewWithParams(mapOf( 17 | "server" to "server", 18 | "account" to Account( 19 | id = 1, 20 | displayName = "displayName", 21 | userName = "username"))) 22 | 23 | FxAssert.verifyThat("#display-name", NodeMatchers.hasText("displayName")) 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/NotificationFragmentTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.TornadoFxTest 4 | import com.sys1yagi.mastodon4j.api.entity.Account 5 | import com.sys1yagi.mastodon4j.api.entity.Notification 6 | import mu.KLogging 7 | import org.junit.Test 8 | 9 | class NotificationFragmentTest : TornadoFxTest() { 10 | companion object : KLogging() 11 | 12 | @Test 13 | fun test_follow() { 14 | showViewWithParams(mapOf( 15 | "server" to "server", 16 | "notification" to Notification( 17 | account = Account( 18 | id = 1, 19 | displayName = "displayName", 20 | userName = "username"), 21 | type = "follow"))) 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/OkHttpLogger.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import mu.KLogging 4 | import okhttp3.Interceptor 5 | import okhttp3.Response 6 | import java.io.IOException 7 | 8 | class OkHttpLogger : Interceptor { 9 | companion object : KLogging() 10 | 11 | @Throws(IOException::class) 12 | override fun intercept(chain: Interceptor.Chain): Response { 13 | val request = chain.request() 14 | 15 | val t1 = System.nanoTime() 16 | logger.debug(String.format("Sending request %s on %s%n%s", 17 | request.url(), chain.connection(), request.headers())) 18 | 19 | val response = chain.proceed(request) 20 | 21 | val t2 = System.nanoTime() 22 | logger.trace(String.format("Received " + response.code() + " response for %s in %.1fms%n%s", 23 | response.request().url(), (t2 - t1) / 1e6, response.headers())) 24 | 25 | return response 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/RegisterApp.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.Scope 5 | import com.sys1yagi.mastodon4j.api.entity.auth.AppRegistration 6 | import com.sys1yagi.mastodon4j.api.exception.Mastodon4jRequestException 7 | import com.sys1yagi.mastodon4j.api.method.Apps 8 | import mu.KotlinLogging 9 | 10 | private val logger = KotlinLogging.logger {} 11 | fun registerApp(mastodonClient: MastodonClient): AppRegistration? { 12 | val apps = Apps(mastodonClient) 13 | try { 14 | val appRegistration = apps.createApp( 15 | "mastodon-jfx", 16 | "urn:ietf:wg:oauth:2.0:oob", 17 | Scope(Scope.Name.ALL), 18 | "https://github.com/wakingrufus").execute() 19 | return appRegistration 20 | 21 | } catch (e: Mastodon4jRequestException) { 22 | logger.error("error", e) 23 | } 24 | return null 25 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/TootEditorTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.TornadoFxTest 4 | import com.nhaarman.mockito_kotlin.mock 5 | import com.sys1yagi.mastodon4j.api.entity.Status 6 | import mu.KLogging 7 | import org.junit.Test 8 | import kotlin.test.assertEquals 9 | 10 | class TootEditorTest : TornadoFxTest() { 11 | companion object : KLogging() 12 | 13 | @Test 14 | fun render() { 15 | val mockClientCall: (String) -> Status = { textBoxString: String -> 16 | Status(content = textBoxString) 17 | } 18 | val tootEditor = showViewWithParams(mapOf( 19 | "client" to mock { }, 20 | "toot" to mockClientCall, 21 | "inReplyToId" to null)) 22 | 23 | clickOn("#toot-editor").write("test") 24 | clickOn("#toot-submit") 25 | 26 | assertEquals( 27 | expected = "test", 28 | actual = tootEditor.createdToot?.content) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/GetAccessToken.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.github.wakingrufus.mastodon.data.OAuthModel 4 | import com.sys1yagi.mastodon4j.MastodonClient 5 | import com.sys1yagi.mastodon4j.api.entity.auth.AccessToken 6 | import com.sys1yagi.mastodon4j.api.exception.Mastodon4jRequestException 7 | import com.sys1yagi.mastodon4j.api.method.Apps 8 | import mu.KotlinLogging 9 | 10 | private val logger = KotlinLogging.logger {} 11 | fun getAccessToken(mastodonClient: MastodonClient, clientId: String, clientSecret: String, authCode: String): AccessToken { 12 | val apps = Apps(mastodonClient) 13 | try { 14 | return apps.getAccessToken(clientId = clientId, clientSecret = clientSecret, code = authCode).execute() 15 | } catch (e: Mastodon4jRequestException) { 16 | logger.error("error getting access token: " + e.localizedMessage, e) 17 | throw e 18 | } 19 | } 20 | 21 | fun getAccessToken(oAuth : OAuthModel): AccessToken = 22 | getAccessToken(oAuth.client,oAuth.appRegistration.clientId, oAuth.appRegistration.clientSecret, oAuth.token!!) -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/IntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon 2 | 3 | import com.github.wakingrufus.mastodon.config.ConfigData 4 | import com.github.wakingrufus.mastodon.config.ConfigurationHandler 5 | import com.nhaarman.mockito_kotlin.doReturn 6 | import com.nhaarman.mockito_kotlin.mock 7 | import javafx.stage.Stage 8 | import mu.KLogging 9 | import org.junit.Test 10 | import org.testfx.framework.junit.ApplicationTest 11 | 12 | class IntegrationTest : ApplicationTest() { 13 | 14 | companion object : KLogging() 15 | 16 | override fun start(stage: Stage) { 17 | val config: ConfigData = mock() 18 | val configHandler = mock { 19 | on { readFileConfig() } doReturn config 20 | } 21 | MastodonApplication().start(stage) 22 | } 23 | 24 | @Test 25 | fun should_drag_file_into_trashcan() { 26 | /* 27 | // given: 28 | 29 | 30 | // when: 31 | clickOn("#newIdButton"); 32 | 33 | // then: 34 | verifyThat("#desktop", hasChildren(0, ".file")); 35 | */ 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/AccountChooserView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.data.AccountState 4 | import com.sys1yagi.mastodon4j.api.entity.Account 5 | import javafx.collections.ObservableList 6 | import mu.KLogging 7 | import tornadofx.* 8 | 9 | class AccountChooserView : View() { 10 | companion object : KLogging() 11 | 12 | val accountFragment: (String, Account) -> AccountFragment by param( 13 | { s: String, a: Account -> find(params = mapOf("server" to s, "account" to a)) }) 14 | val accounts: ObservableList by param() 15 | var choice: AccountState? = null 16 | 17 | override val root = vbox { 18 | id = "account-choices" 19 | children.bind(accounts) { 20 | button { 21 | id = "account-choice-" + it.account.userName + "@" + it.client.getInstanceName() 22 | graphic = accountFragment(it.client.getInstanceName(), it.account).root 23 | action { 24 | choice = it 25 | close() 26 | } 27 | } 28 | } 29 | } 30 | 31 | fun getAccount(): AccountState? = choice 32 | 33 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/account/CreateAccountStateKtTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.account 2 | 3 | import com.github.wakingrufus.mastodon.waitFor 4 | import com.nhaarman.mockito_kotlin.doReturn 5 | import com.nhaarman.mockito_kotlin.mock 6 | import com.sys1yagi.mastodon4j.MastodonRequest 7 | import com.sys1yagi.mastodon4j.api.Pageable 8 | import com.sys1yagi.mastodon4j.api.entity.Status 9 | import javafx.collections.FXCollections 10 | import javafx.collections.ObservableList 11 | import javafx.embed.swing.JFXPanel 12 | import org.junit.Test 13 | 14 | class CreateAccountStateKtTest { 15 | 16 | @Test 17 | fun createAccountState() { 18 | } 19 | 20 | @Test 21 | fun monitorPublicFeed() { 22 | } 23 | 24 | @Test 25 | fun monitorUserFeeds() { 26 | } 27 | 28 | @Test 29 | fun test_fetchAndAddToFeed() { 30 | JFXPanel() 31 | val request = mock>> { 32 | onGeneric { execute() } doReturn Pageable(part = listOf(Status()), link = null) 33 | } 34 | 35 | val feed: ObservableList = FXCollections.observableArrayList() 36 | 37 | fetchAndAddToFeed( 38 | feed = feed, 39 | fetcher = { request }) 40 | waitFor({ feed.size > 0 }) 41 | kotlin.test.assertEquals(1, feed.size) 42 | } 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mastodon-jfx 2 | mastodon-jfx is a desktop client for [Mastodon](joinmastodon.org) 3 | written in kotlin using [TornadoFX](https://github.com/edvin/tornadofx). 4 | It is designed for power tooters, particularly ones with multiple accounts spanning multiple instances. 5 | It is currently in an Alpha state. It is not yet feature-complete and the Look & Feel is in progress. 6 | 7 | [![Run Status](https://api.shippable.com/projects/5963fce801ed240700ba5431/badge?branch=master)](https://app.shippable.com/github/wakingrufus/mastodon-jfx) 8 | [![Coverage Badge](https://api.shippable.com/projects/5963fce801ed240700ba5431/coverageBadge?branch=master)](https://app.shippable.com/github/wakingrufus/mastodon-jfx) 9 | 10 | mastodon-jfx is available as a portable zip or a *.deb package. 11 | Download mastodon-jfx [here](https://github.com/wakingrufus/mastodon-jfx/wiki/Download) 12 | 13 | ### Screenshots 14 | 15 | #### Main UI 16 | ![Main Screenshot](docs/main.png) 17 | 18 | #### Authoring a toot 19 | ![Toot Screenshot](docs/toot.png) 20 | 21 | #### Multiple Accounts 22 | ![Toot Screenshot](docs/multiple-accounts.png) 23 | 24 | ### Implemented features: 25 | - Log in to multiple accounts 26 | - View Home, public, and federated feeds (with real-time updates) 27 | - view notifications 28 | - boost/unboost toots 29 | - favorite/unfavorite toots 30 | - post toots 31 | - reply to toots 32 | 33 | ### Not yet implemented: 34 | - view account 35 | - view #tag feed 36 | - scroll back history -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/TornadoFxTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon 2 | 3 | import javafx.application.Platform 4 | import javafx.scene.Scene 5 | import javafx.stage.Stage 6 | import org.testfx.framework.junit.ApplicationTest 7 | import tornadofx.* 8 | import java.time.Instant 9 | 10 | open class TornadoFxTest : ApplicationTest() { 11 | lateinit var wrapper: TestView 12 | override fun start(stage: Stage) { 13 | wrapper = TestView() 14 | val scene = Scene(wrapper.root, 800.0, 600.0) 15 | stage.scene = scene 16 | stage.show() 17 | } 18 | 19 | protected inline fun showViewWithParams(params: Map<*, Any?>?) = wrapper.addViewWithParams(params) 20 | 21 | class TestView : View() { 22 | override val root = stackpane { } 23 | inline fun findView(params: Map<*, Any?>?) = find(params) 24 | inline fun addViewWithParams(params: Map<*, Any?>?): T { 25 | Platform.runLater { 26 | root.add(findView(params)) 27 | } 28 | waitFor(condition = { root.children.size > 0 }) 29 | return root.children[0].uiComponent()!! 30 | } 31 | } 32 | } 33 | 34 | fun waitFor(condition: () -> Boolean, maxMillis: Long = 10000) { 35 | val startTime = Instant.now() 36 | while (!condition() && Instant.now().isBefore(startTime.plusMillis(maxMillis))) { 37 | Thread.sleep(1000) 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/data/AccountState.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.data 2 | 3 | import com.sys1yagi.mastodon4j.MastodonClient 4 | import com.sys1yagi.mastodon4j.api.entity.Account 5 | import com.sys1yagi.mastodon4j.api.entity.Notification 6 | import com.sys1yagi.mastodon4j.api.entity.Status 7 | import javafx.collections.FXCollections 8 | import javafx.collections.ObservableList 9 | 10 | data class AccountState(val account: Account, 11 | val client: MastodonClient, 12 | val homeFeed: StatusFeed = StatusFeed( 13 | name = "Home", 14 | server = client.getInstanceName(), 15 | statuses = FXCollections.observableArrayList()), 16 | val publicFeed: StatusFeed = StatusFeed( 17 | name = "Public", 18 | server = client.getInstanceName(), 19 | statuses = FXCollections.observableArrayList()), 20 | val federatedFeed: StatusFeed = StatusFeed( 21 | name = "Federated", 22 | server = client.getInstanceName(), 23 | statuses = FXCollections.observableArrayList()), 24 | val notificationFeed: NotificationFeed = NotificationFeed( 25 | name = "Notifications", 26 | server = client.getInstanceName(), 27 | notifications = FXCollections.observableArrayList())) { 28 | override fun toString(): String { 29 | return account.displayName + "@" + client.getInstanceName() 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/AccountChooserViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.data.AccountState 4 | import com.nhaarman.mockito_kotlin.doReturn 5 | import com.nhaarman.mockito_kotlin.mock 6 | import com.sys1yagi.mastodon4j.api.entity.Account 7 | import javafx.collections.FXCollections 8 | import javafx.stage.Stage 9 | import mu.KLogging 10 | import org.junit.Test 11 | import org.testfx.api.FxAssert.verifyThat 12 | import org.testfx.framework.junit.ApplicationTest 13 | import org.testfx.matcher.base.NodeMatchers 14 | import tornadofx.App 15 | import tornadofx.View 16 | import tornadofx.plusAssign 17 | import tornadofx.stackpane 18 | 19 | 20 | class AccountChooserViewTest : ApplicationTest() { 21 | companion object : KLogging() 22 | override fun start(stage: Stage) { 23 | val app = App(TestView::class) 24 | app.start(stage) 25 | } 26 | 27 | class TestView : View() { 28 | override val root = stackpane { 29 | val account: Account = mock { 30 | on { displayName } doReturn "displayName" 31 | on { url } doReturn "" 32 | on { userName } doReturn "userName" 33 | } 34 | val accountState = AccountState( 35 | account = account, 36 | client = mock { on { getInstanceName() } doReturn "instanceName" }) 37 | val accounts = FXCollections.observableArrayList() 38 | accounts.add(accountState) 39 | this += find(mapOf("accounts" to accounts)).root 40 | } 41 | } 42 | 43 | @Test 44 | fun should_contain_button() { 45 | // expect: 46 | verifyThat("#account-choices", NodeMatchers.hasChildren(1, "#account-choice-userName@instanceName")) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/config/FileConfigurationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.config 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.KotlinModule 5 | import com.fasterxml.jackson.module.paramnames.ParameterNamesModule 6 | import mu.KLogging 7 | import java.io.File 8 | import java.io.IOException 9 | 10 | class FileConfigurationHandler(val file: File = File(File(System.getProperty("user.home")), ".mastodon.txt")) 11 | : ConfigurationHandler { 12 | companion object : KLogging() { 13 | val objectMapper = ObjectMapper() 14 | .registerModule(ParameterNamesModule()) 15 | .registerModule(KotlinModule()) 16 | } 17 | 18 | override fun readFileConfig(): ConfigData { 19 | var configData: ConfigData? = null 20 | if (!file.exists()) { 21 | logger.info("config not found, creating") 22 | try { 23 | file.createNewFile() 24 | configData = ConfigData() 25 | } catch (e: IOException) { 26 | logger.error("Error creating config file: " + e.localizedMessage, e) 27 | } 28 | 29 | } else { 30 | configData = try { 31 | objectMapper.readValue(file, ConfigData::class.java) 32 | } catch (e: IOException) { 33 | logger.error("Error reading config file: " + e.localizedMessage, e) 34 | ConfigData() 35 | } 36 | } 37 | return configData!! 38 | } 39 | 40 | override fun saveConfig(configData: ConfigData) { 41 | objectMapper.writeValue(file, configData) 42 | } 43 | 44 | 45 | override fun addAccountToConfig(configData: ConfigData, identity: AccountConfig): ConfigData = 46 | configData.copy(identities = configData.identities.plus(identity)) 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/NotificationFeedView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.data.NotificationFeed 4 | import javafx.geometry.Pos 5 | import javafx.scene.control.ScrollPane 6 | import javafx.scene.paint.Color 7 | import mu.KLogging 8 | import tornadofx.* 9 | 10 | class NotificationFeedView : View() { 11 | companion object : KLogging() 12 | 13 | val notificationFeed: NotificationFeed by param() 14 | 15 | override val root = hbox { 16 | style { 17 | minWidth = 100.percent 18 | minHeight = 100.percent 19 | backgroundColor = multi(Color.rgb(0x06, 0x10, 0x18)) 20 | padding = CssBox(top = 1.px, right = 1.px, bottom = 1.px, left = 1.px) 21 | } 22 | vbox { 23 | style { 24 | backgroundColor = multi(Color.rgb(0x32, 0x8B, 0xDB), 25 | Color.rgb(0x20, 0x7B, 0xCF), 26 | Color.rgb(0x19, 0x73, 0xC9), 27 | Color.rgb(0x0A, 0x65, 0xBF)) 28 | textFill = Color.WHITE 29 | padding = box(1.px, 1.px, 1.px, 1.px) 30 | alignment = Pos.CENTER 31 | maxWidth = 30.em 32 | } 33 | label(notificationFeed.name + " @ " + notificationFeed.server) { 34 | textFill = Color.WHITE 35 | style { 36 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 37 | fontSize = 3.em 38 | } 39 | } 40 | scrollpane { 41 | hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER 42 | vbox { 43 | children.bind(notificationFeed.notifications) { 44 | find(params = mapOf( 45 | "notification" to it, 46 | "server" to notificationFeed.server)).root 47 | } 48 | } 49 | } 50 | } 51 | 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/AccountFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 4 | import com.sys1yagi.mastodon4j.api.entity.Account 5 | import javafx.scene.image.Image 6 | import javafx.scene.image.ImageView 7 | import javafx.scene.paint.Color 8 | import mu.KLogging 9 | import tornadofx.* 10 | import java.net.HttpURLConnection 11 | import java.net.URL 12 | 13 | class AccountFragment : Fragment() { 14 | companion object : KLogging() 15 | 16 | val account: Account by param() 17 | val server: String by param() 18 | lateinit var avatar: ImageView 19 | override val root = hbox { 20 | style { 21 | backgroundColor = multi(DefaultStyles.backgroundColor) 22 | } 23 | avatar = imageview { 24 | fitHeight = 64.px.value 25 | fitWidth = 64.px.value 26 | image = Image(resources["/images/avatar-default.png"]) 27 | } 28 | 29 | vbox { 30 | label(account.displayName) { 31 | id = "display-name" 32 | textFill = Color.WHITE 33 | style { 34 | fontSize = 2.5.em 35 | } 36 | 37 | } 38 | label("@" + account.userName + "@" + server) { 39 | textFill = Color.WHITE 40 | style { 41 | fontSize = 1.5.em 42 | } 43 | } 44 | } 45 | 46 | } 47 | 48 | init { 49 | loadAvatar() 50 | } 51 | 52 | fun loadAvatar() { 53 | runAsync { 54 | try { 55 | val url = URL(account.avatar) 56 | val httpcon: HttpURLConnection = url.openConnection() as HttpURLConnection 57 | httpcon.addRequestProperty("User-Agent", "Mozilla/4.0") 58 | Image(httpcon.inputStream) 59 | } catch (e: Exception) { 60 | logger.warn("error loading avatar: " + account.avatar, e) 61 | Image(resources["/images/avatar-default.png"]) 62 | } 63 | } ui { 64 | avatar.image = it 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /shippable.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | 6 | env: 7 | global: 8 | - CD_BUCKET=mastodon-jfx-release CD_KEY=latest.zip 9 | - secure: z9IGw3NJvhMCVNNDR66STZlpHp2gd62cAfJn/oUe0/ozvD1qcPyl3+c+Ypp06Hv3mFIzJUjaeWbTqs5uCuCGAVjB5shHwhFgO42oW1oH9zEBF1bEbSAoYTrVg+fp9Xw1lHkrStaUh2Sg3zF36Lv591BPCtJqaPPCFSkLLi5sI6c400BoD1vrHgKozd8ysutkmEMoebBwmeoBJj78j4NxINWXjpFp8a3ek0b/GPdsDT6A0kNQtxa1zo7k+sD0JtVvgxwRtkWB6JIWiWTiz7TicHWXhQyES3c7kxXQqdCRMU6Koa1lCOvj9/PFgtlO+7YFyJIFlOMarVgIp/QL6h8Erw== 10 | - secure: dkmc+cm3MJORJsUfg8nceylVgD9Cd6cyiQ+4Rf1SJw3+mLorvBInMlVF96tKbGhPTgAGO4jJZGHoTrC9ygYJF5/t+Z5W2oikt4/FKJtlTj3KIZY7ggpoeJ1u9Oc2OLN1EfuxbByKQNw2yQLbtha5cjufDr39Lgdud8qQv0neNVms9cKELaGB4mcFf6A1PXPniJmcENH+UrlS6EdTgO5TzjzB8jDyyNHoBAmwo4rXNAEZfolJyoULkELGGvzaPMbppR6qse7+VTSlzNh5hORoHPpeImjeNQ7LsC88Cdmn+PXt/ksHhAxUHzIYmZ67SSTI2MuUxvRPNGbyr6fD9neVjg== 11 | 12 | before_script: 13 | - uname -a 14 | - java -version 15 | 16 | build: 17 | ci: 18 | - sudo apt update 19 | - sudo apt-get install openjfx 20 | - Xvfb +extension RANDR :99 &>/dev/null & 21 | - export DISPLAY=:99.0 22 | - mkdir -p shippable/testresults 23 | - mkdir -p shippable/codecoverage 24 | - mkdir -p shippable/codecoverage/target/site/jacoco 25 | - rm -rf build 26 | - gradle wrapper 27 | - ./gradlew clean build 28 | - cp build/test-results/test/*.xml shippable/testresults 29 | - cp build/reports/jacoco/test/jacocoTestReport.xml shippable/codecoverage 30 | - cp -r build/jacoco/test.exec shippable/codecoverage/target/site/jacoco 31 | - cp -r build/classes shippable/codecoverage/target 32 | on_success: 33 | - export TAG_NAME=`git describe --exact-match --tags HEAD` 34 | - export BRANCH_NAME=`git rev-parse --abbrev-ref HEAD` 35 | - if [ "$TAG_NAME" != "" ]; then export RELEASE_KEY="$TAG_NAME.zip"; fi 36 | - if [ "$TAG_NAME" != "" ]; then export RELEASE_KEY_DEB="$TAG_NAME.deb"; fi 37 | - if [ "$BRANCH_NAME" = "master" ]; then aws s3 cp $SHIPPABLE_BUILD_DIR/build/distributions/mastodon-jfx-*.zip s3://$CD_BUCKET/$CD_KEY; fi 38 | - if [ "$TAG_NAME" != "" ]; then aws s3 cp $SHIPPABLE_BUILD_DIR/build/distributions/mastodon-jfx-*.zip s3://$CD_BUCKET/$RELEASE_KEY; fi 39 | - if [ "$TAG_NAME" != "" ]; then aws s3 cp $SHIPPABLE_BUILD_DIR/build/jfx/native/mastodon-jfx-*.deb s3://$CD_BUCKET/$RELEASE_KEY_DEB; fi -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/TootEditor.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.client.parseUrl 4 | import com.github.wakingrufus.mastodon.toot.createToot 5 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 6 | import com.sys1yagi.mastodon4j.MastodonClient 7 | import com.sys1yagi.mastodon4j.api.entity.Status 8 | import javafx.scene.paint.Color 9 | import mu.KLogging 10 | import tornadofx.* 11 | 12 | class TootEditor : Fragment() { 13 | companion object : KLogging() 14 | 15 | val client: MastodonClient by param() 16 | val inReplyTo: Status? by param() 17 | val toot: (String) -> Status by param({ tootString -> 18 | createToot( 19 | client = client, 20 | status = tootString, 21 | inReplyToId = inReplyTo?.id) 22 | }) 23 | val parseUrlFunc: (String) -> String by param(defaultValue = ::parseUrl) 24 | 25 | var createdToot: Status? = null 26 | 27 | 28 | override val root = vbox { 29 | style { 30 | backgroundColor = multi(DefaultStyles.backgroundColor) 31 | } 32 | inReplyTo?.let { 33 | label("Replying to:") { 34 | style { 35 | fontSize = 2.em 36 | minWidth = 11.em 37 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 38 | textFill = DefaultStyles.armedTextColor 39 | } 40 | } 41 | this += find(mapOf( 42 | "account" to it.account!!, 43 | "server" to parseUrlFunc(it.uri))) 44 | val toot = parseToot(it.content) 45 | toot.style { 46 | backgroundColor = multi(DefaultStyles.backgroundColor) 47 | textFill = Color.WHITE 48 | } 49 | this += toot 50 | } 51 | val tootText = textarea { 52 | id = "toot-editor" 53 | } 54 | buttonbar { 55 | button("Toot") { 56 | addClass(DefaultStyles.smallButton) 57 | id = "toot-submit" 58 | action { 59 | createdToot = toot(tootText.text) 60 | close() 61 | } 62 | } 63 | button("Close") { 64 | addClass(DefaultStyles.smallButton) 65 | action { 66 | close() 67 | } 68 | } 69 | } 70 | } 71 | 72 | 73 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/NotificationFeedViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.TornadoFxTest 4 | import com.github.wakingrufus.mastodon.data.NotificationFeed 5 | import com.sys1yagi.mastodon4j.api.entity.Account 6 | import com.sys1yagi.mastodon4j.api.entity.Notification 7 | import com.sys1yagi.mastodon4j.api.entity.Status 8 | import javafx.collections.FXCollections 9 | import mu.KLogging 10 | import org.junit.Test 11 | 12 | class NotificationFeedViewTest : TornadoFxTest() { 13 | companion object : KLogging() 14 | 15 | @Test 16 | fun test() { 17 | val account = Account( 18 | id = 1, 19 | displayName = "displayName", 20 | userName = "username") 21 | showViewWithParams(mapOf( 22 | "notificationFeed" to NotificationFeed( 23 | name = "userName", 24 | server = "http://mastodon.social", 25 | notifications = FXCollections.observableArrayList( 26 | Notification( 27 | account = account, 28 | type = Notification.Type.Follow.value), 29 | Notification( 30 | status = Status( 31 | content = "

toot

", 32 | account = account), 33 | account = account, 34 | type = Notification.Type.Favourite.value), 35 | Notification( 36 | status = Status( 37 | content = "

toot

", 38 | account = account), 39 | account = account, 40 | type = Notification.Type.Mention.value), 41 | Notification( 42 | status = Status( 43 | content = "

toot

", 44 | account = account), 45 | account = account, 46 | type = Notification.Type.Reblog.value) 47 | ) 48 | ) 49 | )) 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/client/OAuthActions.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.client 2 | 3 | import com.github.wakingrufus.mastodon.account.createAccountConfig 4 | import com.github.wakingrufus.mastodon.account.createAccountState 5 | import com.github.wakingrufus.mastodon.config.AccountConfig 6 | import com.github.wakingrufus.mastodon.config.ConfigurationHandler 7 | import com.github.wakingrufus.mastodon.config.FileConfigurationHandler 8 | import com.github.wakingrufus.mastodon.data.AccountState 9 | import com.github.wakingrufus.mastodon.data.OAuthModel 10 | import com.sys1yagi.mastodon4j.MastodonClient 11 | import com.sys1yagi.mastodon4j.api.Scope 12 | import com.sys1yagi.mastodon4j.api.entity.Notification 13 | import com.sys1yagi.mastodon4j.api.entity.auth.AccessToken 14 | import com.sys1yagi.mastodon4j.api.method.Accounts 15 | import com.sys1yagi.mastodon4j.api.method.Apps 16 | import mu.KotlinLogging 17 | 18 | private val logger = KotlinLogging.logger {} 19 | fun getOAuthUrl(oauthModel: OAuthModel): String { 20 | val apps = Apps(oauthModel.client) 21 | return apps.getOAuthUrl( 22 | clientId = oauthModel.appRegistration.clientId, 23 | scope = Scope(Scope.Name.ALL), 24 | redirectUri = oauthModel.appRegistration.redirectUri) 25 | } 26 | 27 | fun completeOAuth(oAuth: OAuthModel, 28 | onComplete: (AccountState) -> Unit, 29 | configHandler: ConfigurationHandler = FileConfigurationHandler(), 30 | accountStateCreator: (MastodonClient, (Notification) -> Unit) -> AccountState = ::createAccountState, 31 | accountConfigCreator: (Accounts, String, String, String, String) -> AccountConfig = ::createAccountConfig, 32 | accessTokenBuilder: (OAuthModel) -> AccessToken = ::getAccessToken, 33 | accountClientCreator: (String, String) -> MastodonClient = ::createAccountClient) { 34 | val accessToken = accessTokenBuilder.invoke(oAuth) 35 | val accountClient = accountClientCreator.invoke(oAuth.client.getInstanceName(), accessToken.accessToken) 36 | val accountState = accountStateCreator.invoke(accountClient, {System.out.println(it.toString())}) 37 | configHandler.saveConfig(configHandler.addAccountToConfig( 38 | configHandler.readFileConfig(), 39 | accountConfigCreator.invoke( 40 | Accounts(accountClient), 41 | accessToken.accessToken, 42 | oAuth.appRegistration.clientId, 43 | oAuth.appRegistration.clientSecret, 44 | accountClient.getInstanceName()))) 45 | onComplete.invoke(accountState) 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/styles/DefaultStyles.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui.styles 2 | 3 | import javafx.scene.Cursor 4 | import javafx.scene.layout.BorderStrokeStyle 5 | import javafx.scene.paint.Color 6 | import tornadofx.* 7 | 8 | 9 | class DefaultStyles : Stylesheet() { 10 | companion object { 11 | val smallButton by cssclass() 12 | val textInputLabel by cssclass() 13 | val textInput by cssclass() 14 | val defaultBorder by cssclass() 15 | 16 | val backdropColor = c ("#222222") 17 | val backgroundColor = c("#122e43") 18 | val darkestBackgroundColor = c("#061018") 19 | val darkerBackgroundColor = c("#0E2333") 20 | val linkColor = c("#AAAAFF") 21 | 22 | private val presetBackgroundColor = c("#061018") 23 | private val armedBackgroundColor = c("#2b6a9b") 24 | private val hoverBackgroundColor = c("#122e43") 25 | val textColor = c("#2b6a9b") 26 | val armedTextColor = Color.WHITE 27 | 28 | } 29 | 30 | init { 31 | smallButton { 32 | backgroundColor = multi(presetBackgroundColor) 33 | textFill = textColor 34 | backgroundRadius = multi(CssBox(10.px, 10.px, 10.px, 10.px)) 35 | backgroundInsets = multi(CssBox(0.px, 0.px, 0.px, 0.px)) 36 | fontSize = 3.em 37 | cursor = Cursor.HAND 38 | and(armed) { 39 | backgroundColor = multi(armedBackgroundColor) 40 | textFill = armedTextColor 41 | } 42 | and(hover) { 43 | backgroundColor = multi(hoverBackgroundColor) 44 | textFill = textColor 45 | } 46 | } 47 | textInputLabel { 48 | fontSize = 2.em 49 | minWidth = 10.em 50 | maxWidth = 50.percent 51 | } 52 | textInputLabel { 53 | fontSize = 3.em 54 | minWidth = 10.em 55 | maxWidth = 50.percent 56 | textFill = Color.WHITE 57 | } 58 | defaultBorder { 59 | borderColor = multi(CssBox( 60 | top = DefaultStyles.darkestBackgroundColor, 61 | bottom = DefaultStyles.darkestBackgroundColor, 62 | left = DefaultStyles.darkestBackgroundColor, 63 | right = DefaultStyles.darkestBackgroundColor 64 | )) 65 | borderWidth = multi(CssBox( 66 | top = .2.em, 67 | bottom = .2.em, 68 | left = .2.em, 69 | right = .2.em)) 70 | borderStyle = multi(BorderStrokeStyle.SOLID) 71 | } 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/OAuthViewTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.TornadoFxTest 4 | import com.github.wakingrufus.mastodon.data.AccountState 5 | import com.github.wakingrufus.mastodon.data.OAuthModel 6 | import com.github.wakingrufus.mastodon.waitFor 7 | import com.nhaarman.mockito_kotlin.mock 8 | import com.sys1yagi.mastodon4j.api.entity.auth.AppRegistration 9 | import mu.KLogging 10 | import org.junit.Test 11 | import org.testfx.api.FxAssert 12 | import org.testfx.matcher.base.NodeMatchers 13 | import kotlin.test.assertEquals 14 | 15 | class OAuthViewTest : TornadoFxTest() { 16 | companion object : KLogging() 17 | 18 | @Test 19 | fun render() { 20 | val onComplete: (AccountState) -> Unit = {} 21 | showViewWithParams(mapOf( 22 | "buildModel" to { OAuthModel(appRegistration = AppRegistration(), client = mock {}) }, 23 | "onComplete" to onComplete, 24 | "oAuthUrlBuilder" to { "url" }, 25 | "completeOAuthFunction" to { onComplete })) 26 | 27 | FxAssert.verifyThat("#instance-field", NodeMatchers.hasText("")) 28 | // expect: 29 | FxAssert.verifyThat("#instance-form", NodeMatchers.hasChildren(1, "#instance-field-label")) 30 | FxAssert.verifyThat("#instance-form", NodeMatchers.hasChildren(1, "#instance-field")) 31 | FxAssert.verifyThat("#instance-form", NodeMatchers.hasChildren(1, "#instance-form-submit")) 32 | FxAssert.verifyThat("#oauth-root", NodeMatchers.hasChildren(1, "#login-wrapper")) 33 | FxAssert.verifyThat("#login-wrapper", NodeMatchers.hasChildren(0, "#instance-login")) 34 | 35 | } 36 | 37 | @Test 38 | fun showLogin() { 39 | val onComplete: (AccountState) -> Unit = {} 40 | showViewWithParams(mapOf( 41 | "buildModel" to { url: String -> 42 | assertEquals("url", url) 43 | OAuthModel(appRegistration = AppRegistration(), client = mock {}) 44 | }, 45 | "onComplete" to onComplete, 46 | "oAuthUrlBuilder" to { "url" }, 47 | "completeOAuthFunction" to { onComplete })) 48 | 49 | FxAssert.verifyThat("#login-wrapper", NodeMatchers.hasChildren(0, "#instance-login")) 50 | FxAssert.verifyThat("#instance-field", NodeMatchers.hasText("")) 51 | clickOn("#instance-field").write("url") 52 | clickOn("#instance-form-submit") 53 | waitFor(condition = { false }, maxMillis = 2000) 54 | // expect: 55 | FxAssert.verifyThat("#login-wrapper", NodeMatchers.hasChildren(1, "#instance-login")) 56 | FxAssert.verifyThat("#login-wrapper", NodeMatchers.hasChildren(1, "#token-form")) 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/ParseTootContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 4 | import javafx.scene.Node 5 | import javafx.scene.control.Hyperlink 6 | import javafx.scene.layout.Pane 7 | import javafx.scene.layout.VBox 8 | import javafx.scene.paint.Color 9 | import javafx.scene.text.Text 10 | import javafx.scene.text.TextFlow 11 | import mu.KotlinLogging 12 | import org.jsoup.Jsoup 13 | import org.jsoup.nodes.Document 14 | import org.jsoup.nodes.Element 15 | import org.jsoup.nodes.TextNode 16 | import tornadofx.multi 17 | import tornadofx.style 18 | import tornadofx.tooltip 19 | import java.awt.Desktop 20 | import java.net.URI 21 | 22 | private val logger = KotlinLogging.logger {} 23 | fun parseToot(content: String): Pane { 24 | // logger.info { content } 25 | val tootContainer = VBox() 26 | val document: Document = Jsoup.parse(content) 27 | val body: Element = document.body() 28 | tootContainer.children.add(parseNode(body)) 29 | return tootContainer 30 | } 31 | 32 | fun parseNode(htmlNode: org.jsoup.nodes.Node): Node { 33 | var node: Node? = null 34 | // logger.info { "${htmlNode.nodeName()} has ${htmlNode.childNodes().size} children" } 35 | 36 | if (htmlNode is Element && (htmlNode).tagName() == "a") { 37 | // logger.debug { "processing : ${htmlNode.text()}" } 38 | // logger.debug { "href: ${htmlNode.attr("href")}" } 39 | val link = Hyperlink(htmlNode.text()) 40 | link.style { 41 | fill = DefaultStyles.linkColor 42 | textFill = DefaultStyles.linkColor 43 | } 44 | link.tooltip(text = htmlNode.attr("href")) 45 | link.setOnAction { _ -> 46 | if (Desktop.isDesktopSupported()) { 47 | Desktop.getDesktop().browse(URI(htmlNode.attr("href"))) 48 | } 49 | } 50 | node = link 51 | } else if (htmlNode is TextNode) { 52 | // logger.debug { "processing text: ${htmlNode.text()}" } 53 | node = Text(htmlNode.text()) 54 | node.style { 55 | fill = Color.WHITE 56 | textFill = Color.WHITE 57 | } 58 | } else if (htmlNode.childNodes().size > 0) { 59 | val hbox = if (htmlNode is Element && htmlNode.tagName() == "p") TextFlow() else VBox() 60 | // hbox.maxWidth(30.0) 61 | hbox.style { 62 | backgroundColor = multi(DefaultStyles.darkerBackgroundColor) 63 | // minWidth = 28.em 64 | } 65 | htmlNode.childNodes().forEach { hbox.children.add(parseNode(it)) } 66 | node = hbox 67 | } else if (htmlNode is Element && htmlNode.tagName() == "br") { 68 | // logger.debug { "processing line break" } 69 | node = Text(" \n") 70 | } else { 71 | logger.warn { "missing case: " + htmlNode.nodeName() } 72 | } 73 | return node!! 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/MainView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.account.createAccountState 4 | import com.github.wakingrufus.mastodon.client.createAccountClient 5 | import com.github.wakingrufus.mastodon.config.ConfigurationHandler 6 | import com.github.wakingrufus.mastodon.config.FileConfigurationHandler 7 | import com.github.wakingrufus.mastodon.data.AccountState 8 | import com.github.wakingrufus.mastodon.data.NotificationFeed 9 | import com.github.wakingrufus.mastodon.data.StatusFeed 10 | import javafx.collections.FXCollections 11 | import mu.KLogging 12 | import org.controlsfx.control.Notifications 13 | import tornadofx.* 14 | import java.io.File 15 | 16 | class MainView : View() { 17 | companion object : KLogging() 18 | 19 | val accountStates = FXCollections.observableArrayList() 20 | val statusFeeds = FXCollections.observableArrayList() 21 | val settingsView = find(mapOf( 22 | "accountStates" to accountStates, 23 | "createAccount" to this::newAccount, 24 | "viewNotifications" to this::viewNotifications, 25 | "viewFeed" to this::viewFeed)) 26 | val statusFeedsView = find(mapOf( 27 | "statusFeeds" to statusFeeds, 28 | "accounts" to accountStates)) 29 | 30 | val configHandler: ConfigurationHandler = 31 | FileConfigurationHandler(File(File(System.getProperty("user.home")), ".mastodon.txt")) 32 | 33 | override val root = borderpane { 34 | minHeight = 100.percent.value 35 | // center(FeedsController::class) 36 | left = settingsView.root 37 | 38 | } 39 | 40 | fun viewFeed(statusFeed: StatusFeed) { 41 | statusFeeds.add(statusFeed) 42 | viewFeeds() 43 | } 44 | 45 | fun viewNotifications(notificationFeed: NotificationFeed) { 46 | root.right = find(mapOf("notificationFeed" to notificationFeed)).root 47 | } 48 | 49 | fun viewFeeds() { 50 | root.center = statusFeedsView.root 51 | } 52 | 53 | fun newAccount() { 54 | root.center = find(mapOf("onComplete" to { a: AccountState -> accountStates.add(a) })).root 55 | } 56 | 57 | init { 58 | // controller.readConfig() 59 | configHandler.readFileConfig().identities.forEach { (accessToken, _, _, _, server) -> 60 | val client = createAccountClient(server, accessToken) 61 | accountStates.add(createAccountState(client = client, onNotification = { 62 | Notifications.create() 63 | // .text(it.type) 64 | .graphic(find(params = mapOf("notification" to it, "server" to server)).root) 65 | .darkStyle() 66 | .show() 67 | })) 68 | } 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/github/wakingrufus/mastodon/ui/ParseTootContentKtTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import javafx.embed.swing.JFXPanel 4 | import javafx.scene.Parent 5 | import javafx.scene.Scene 6 | import javafx.scene.control.Hyperlink 7 | import javafx.scene.layout.HBox 8 | import javafx.scene.layout.Pane 9 | import javafx.scene.layout.VBox 10 | import javafx.scene.text.Text 11 | import javafx.scene.text.TextFlow 12 | import javafx.stage.Stage 13 | import mu.KLogging 14 | import org.junit.Test 15 | import org.testfx.framework.junit.ApplicationTest 16 | import kotlin.test.assertEquals 17 | 18 | class ParseTootContentKtTest: ApplicationTest() { 19 | companion object : KLogging() 20 | 21 | val sampleToot1: String = "

Fishing

text before link https://www.gamingonlinux.com/articles/fishing-planet-leaves-early-access-with-full-linux-support-also-has-the-unity-fullscreen-bug.10242 #Linux #LinuxGaming

" 22 | val sampleToot2: String = "

RT @plsburydoughboy Is it gay to dive into the Marianas Trench you're entering a power bottom

\n" 23 | 24 | @Test 25 | fun parseToot() { 26 | val actual: Pane = parseToot(content = sampleToot1) 27 | logger.info { actual.toString() } 28 | val node: VBox = actual.children[0] as VBox 29 | logger.info { node.toString() } 30 | assertEquals(2, node.children.size, "2 paragraphs") 31 | val par1: TextFlow = node.children[0] as TextFlow 32 | val par1Text = par1.children[0] as Text 33 | assertEquals("Fishing", par1Text.text) 34 | val par2: TextFlow = node.children[1] as TextFlow 35 | // assertEquals(4, node.children.size, "2nd paragraph has 4 children") 36 | val par2Text: Text = par2.children[0] as Text 37 | assertEquals("text before link ", par2Text.text) 38 | val par2link1: Hyperlink = par2.children[1] as Hyperlink 39 | assertEquals("https://www.gamingonlinux.com/articles/fishing-planet-leaves-early-access-with-full-linux-support-also-has-the-unity-fullscreen-bug.10242", 40 | par2link1.text) 41 | val par2Text2: Text = par2.children[2] as Text 42 | assertEquals(" ", par2Text2.text) 43 | val par2link2: Hyperlink = par2.children[3] as Hyperlink 44 | assertEquals("#Linux", par2link2.text) 45 | val par2Text3: Text = par2.children[4] as Text 46 | assertEquals(" ", par2Text3.text) 47 | val par2link3: Hyperlink = par2.children[5] as Hyperlink 48 | assertEquals("#LinuxGaming", par2link3.text) 49 | } 50 | 51 | @Throws(Exception::class) 52 | override fun start(stage: Stage) { 53 | JFXPanel() 54 | val load: Parent = HBox() 55 | val scene = Scene(load, 800.0, 600.0) 56 | stage.scene = scene 57 | stage.show() 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/OAuthView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.client.completeOAuth 4 | import com.github.wakingrufus.mastodon.client.createServerClient 5 | import com.github.wakingrufus.mastodon.client.getOAuthUrl 6 | import com.github.wakingrufus.mastodon.client.registerApp 7 | import com.github.wakingrufus.mastodon.data.AccountState 8 | import com.github.wakingrufus.mastodon.data.OAuthModel 9 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 10 | import com.sys1yagi.mastodon4j.MastodonClient 11 | import com.sys1yagi.mastodon4j.api.entity.auth.AppRegistration 12 | import javafx.scene.control.TextField 13 | import javafx.scene.layout.VBox 14 | import javafx.scene.paint.Color 15 | import mu.KLogging 16 | import tornadofx.* 17 | 18 | class OAuthView : Fragment() { 19 | companion object : KLogging() 20 | 21 | val buildModel: (String) -> OAuthModel by param(defaultValue = {serverUrl: String -> 22 | val client = createServerClient(serverUrl) 23 | OAuthModel(appRegistration = registerApp(client)!!, client = client) 24 | }) 25 | val onComplete: (AccountState) -> Unit by param() 26 | val oAuthUrlBuilder: (OAuthModel) -> String by param(::getOAuthUrl) 27 | val completeOAuthFunction: (OAuthModel) -> Unit by param({ model -> 28 | completeOAuth(oAuth = model, onComplete = onComplete) 29 | }) 30 | 31 | lateinit var serverField: TextField 32 | lateinit var vbox: VBox 33 | lateinit var tokenField: TextField 34 | lateinit var loginWrapper: VBox 35 | override val root = vbox { 36 | id = "oauth-root" 37 | style { 38 | minWidth = 100.percent 39 | minHeight = 100.percent 40 | backgroundColor = multi(Color.rgb(0x06, 0x10, 0x18)) 41 | padding = CssBox(top = 1.px, right = 1.px, bottom = 1.px, left = 1.px) 42 | } 43 | hbox { 44 | id = "instance-form" 45 | style { 46 | backgroundColor = multi(DefaultStyles.backgroundColor) 47 | } 48 | label("Server") { 49 | id = "instance-field-label" 50 | addClass(DefaultStyles.textInputLabel) 51 | } 52 | serverField = textfield { 53 | id = "instance-field" 54 | addClass(DefaultStyles.textInput) 55 | } 56 | button("Get Token") { 57 | id = "instance-form-submit" 58 | addClass(DefaultStyles.smallButton) 59 | action { 60 | showLogin(serverField.text) 61 | } 62 | } 63 | } 64 | loginWrapper = vbox { 65 | id = "login-wrapper" 66 | } 67 | } 68 | 69 | fun showLogin(serverUrl: String) { 70 | loginWrapper.clear() 71 | val oAuthModel = buildModel(serverUrl) 72 | loginWrapper += webview { 73 | id = "instance-login" 74 | engine?.load(oAuthUrlBuilder.invoke(oAuthModel)) 75 | } 76 | loginWrapper += hbox { 77 | id = "token-form" 78 | label("Token") { 79 | addClass(DefaultStyles.textInputLabel) 80 | } 81 | tokenField = textfield { 82 | } 83 | button("Login") { 84 | addClass(DefaultStyles.smallButton) 85 | action { 86 | completeOAuthFunction(oAuthModel.copy(token = tokenField.text)) 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/NotificationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 4 | import com.sys1yagi.mastodon4j.api.entity.Notification 5 | import javafx.geometry.Pos 6 | import javafx.scene.paint.Color 7 | import mu.KLogging 8 | import tornadofx.* 9 | 10 | class NotificationFragment : Fragment() { 11 | companion object : KLogging() 12 | 13 | val server: String by param() 14 | val notification: Notification by param() 15 | 16 | override val root = vbox { 17 | hbox { 18 | style { 19 | backgroundColor = multi(DefaultStyles.backgroundColor) 20 | } 21 | label(notification.account?.displayName!!) { 22 | textFill = Color.WHITE 23 | style { 24 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 25 | fontSize = 1.5.em 26 | } 27 | } 28 | if (notification.type == Notification.Type.Follow.value) { 29 | label(" followed you.") { 30 | textFill = Color.WHITE 31 | style { 32 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 33 | fontSize = 1.5.em 34 | } 35 | } 36 | } else if (notification.type == Notification.Type.Reblog.value) { 37 | label(" boosted your toot.") { 38 | textFill = Color.WHITE 39 | style { 40 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 41 | fontSize = 1.5.em 42 | } 43 | } 44 | } else if (notification.type == Notification.Type.Favourite.value) { 45 | label(" favourited your toot") { 46 | textFill = Color.WHITE 47 | style { 48 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 49 | fontSize = 1.5.em 50 | } 51 | } 52 | } else if (notification.type == Notification.Type.Mention.value) { 53 | label(" mentioned you") { 54 | textFill = Color.WHITE 55 | style { 56 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 57 | fontSize = 1.5.em 58 | } 59 | } 60 | } 61 | } 62 | hbox { 63 | if (notification.type == Notification.Type.Follow.value 64 | || notification.type == Notification.Type.Mention.value) 65 | this += find(params = mapOf( 66 | "account" to notification.account, 67 | "server" to server)) 68 | else { 69 | addClass(DefaultStyles.defaultBorder) 70 | style { 71 | padding = CssBox(top = 1.em, bottom = 1.em, left = 1.em, right = 1.em) 72 | alignment = Pos.TOP_LEFT 73 | backgroundColor = multi(DefaultStyles.backgroundColor) 74 | textFill = Color.WHITE 75 | } 76 | val toot = parseToot(notification.status?.content!!) 77 | toot.style { 78 | backgroundColor = multi(DefaultStyles.backgroundColor) 79 | textFill = Color.WHITE 80 | } 81 | this += toot 82 | } 83 | } 84 | label(notification.createdAt) { 85 | textFill = Color.WHITE 86 | style { 87 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 88 | fontSize = 1.em 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/SettingsView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.data.AccountState 4 | import com.github.wakingrufus.mastodon.data.NotificationFeed 5 | import com.github.wakingrufus.mastodon.data.StatusFeed 6 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 7 | import com.sys1yagi.mastodon4j.api.entity.Account 8 | import javafx.collections.ObservableList 9 | import javafx.geometry.Pos 10 | import javafx.scene.paint.Color 11 | import javafx.stage.StageStyle 12 | import mu.KLogging 13 | import tornadofx.* 14 | 15 | class SettingsView(accountFragment: (String, Account) -> AccountFragment = 16 | { s: String, a: Account -> find(params = mapOf("server" to s, "account" to a)) }) : View() { 17 | companion object : KLogging() 18 | 19 | val createAccount: () -> Unit by param() 20 | val accountStates: ObservableList by param() 21 | val viewFeed: (StatusFeed) -> Any by param() 22 | val viewNotifications: (NotificationFeed) -> Any by param() 23 | val newToot: () -> Any by param() 24 | override val root = vbox { 25 | style { 26 | minWidth = 30.em 27 | minHeight = 100.percent 28 | backgroundColor = multi(DefaultStyles.backdropColor) 29 | padding = CssBox(top = 1.px, right = 1.px, bottom = 1.px, left = 1.px) 30 | } 31 | vbox { 32 | style { 33 | id = "accountListWrapper" 34 | textFill = Color.WHITE 35 | minHeight = 100.percent 36 | backgroundColor = multi(DefaultStyles.backgroundColor) 37 | } 38 | children.bind(accountStates) { 39 | hbox { 40 | vbox { 41 | this += accountFragment(it.client.getInstanceName(), it.account) 42 | hbox { 43 | button("⌂") { 44 | addClass(DefaultStyles.smallButton) 45 | accessibleText = "Home" 46 | action { 47 | viewFeed(it.homeFeed) 48 | } 49 | } 50 | button("👥") { 51 | addClass(DefaultStyles.smallButton) 52 | accessibleText = "Public" 53 | action { 54 | viewFeed(it.publicFeed) 55 | } 56 | } 57 | button("🌎") { 58 | addClass(DefaultStyles.smallButton) 59 | accessibleText = "Federated" 60 | action { 61 | viewFeed(it.federatedFeed) 62 | } 63 | } 64 | button("🔔") { 65 | addClass(DefaultStyles.smallButton) 66 | accessibleText = "Notifications" 67 | action { 68 | viewNotifications(it.notificationFeed) 69 | } 70 | } 71 | button("📝") { 72 | addClass(DefaultStyles.smallButton) 73 | accessibleText = "New Toot" 74 | action { 75 | find(mapOf("client" to it.client)).apply { 76 | openModal(stageStyle = StageStyle.UTILITY, block = true) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | hbox { 85 | style { 86 | alignment = Pos.BOTTOM_CENTER 87 | maxHeight = 2.em 88 | minWidth = 100.percent 89 | } 90 | button { 91 | addClass(DefaultStyles.smallButton) 92 | text = "Add" 93 | setOnAction { createAccount() } 94 | } 95 | } 96 | } 97 | 98 | } 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/account/CreateAccountState.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.account 2 | 3 | import com.github.wakingrufus.mastodon.client.buildNotificationsClient 4 | import com.github.wakingrufus.mastodon.client.buildPublicClient 5 | import com.github.wakingrufus.mastodon.client.buildStreamingClient 6 | import com.github.wakingrufus.mastodon.client.buildTimelinesClient 7 | import com.github.wakingrufus.mastodon.data.AccountState 8 | import com.sys1yagi.mastodon4j.MastodonClient 9 | import com.sys1yagi.mastodon4j.MastodonRequest 10 | import com.sys1yagi.mastodon4j.api.Handler 11 | import com.sys1yagi.mastodon4j.api.Pageable 12 | import com.sys1yagi.mastodon4j.api.Range 13 | import com.sys1yagi.mastodon4j.api.Shutdownable 14 | import com.sys1yagi.mastodon4j.api.entity.Notification 15 | import com.sys1yagi.mastodon4j.api.entity.Status 16 | import com.sys1yagi.mastodon4j.api.exception.Mastodon4jRequestException 17 | import com.sys1yagi.mastodon4j.api.method.Accounts 18 | import javafx.application.Platform 19 | import javafx.collections.ObservableList 20 | import kotlinx.coroutines.experimental.CommonPool 21 | import kotlinx.coroutines.experimental.launch 22 | import mu.KotlinLogging 23 | 24 | private val logger = KotlinLogging.logger {} 25 | fun createAccountState(client: MastodonClient, 26 | onNotification: (Notification) -> Unit) 27 | : AccountState { 28 | val accountsClient = Accounts(client) 29 | try { 30 | val account = accountsClient.getVerifyCredentials().execute() 31 | val streamingClient = buildStreamingClient(client) 32 | val newAccountState = AccountState( 33 | account = account, 34 | client = client) 35 | launch(CommonPool) { 36 | logger.debug("fetching Home feed") 37 | 38 | fetchAndAddToFeed(newAccountState.homeFeed.statuses, buildTimelinesClient(client)::getHome) 39 | 40 | val notifications = fetchAndAddToFeed( 41 | newAccountState.notificationFeed.notifications, 42 | buildNotificationsClient(client)::getNotifications) 43 | 44 | logger.info(notifications.size.toString() + " notifications found") 45 | 46 | val shutdownable = monitorUserFeeds( 47 | homeFeed = newAccountState.homeFeed.statuses, 48 | notificationFeed = newAccountState.notificationFeed.notifications, 49 | client = client, 50 | onNotification = onNotification) 51 | 52 | fetchAndAddToFeed(newAccountState.publicFeed.statuses, buildPublicClient(client)::getLocalPublic) 53 | monitorPublicFeed(newAccountState.publicFeed.statuses, streamingClient::localPublic) 54 | 55 | fetchAndAddToFeed(newAccountState.federatedFeed.statuses, buildPublicClient(client)::getFederatedPublic) 56 | monitorPublicFeed(newAccountState.federatedFeed.statuses, streamingClient::federatedPublic) 57 | 58 | } 59 | return newAccountState 60 | } catch (e: Mastodon4jRequestException) { 61 | logger.error("error fetching account: " + e.message, e) 62 | throw Exception("error fetching account: " + e.message) 63 | } 64 | } 65 | 66 | /** 67 | * @return the new items 68 | */ 69 | fun fetchAndAddToFeed(feed: ObservableList, 70 | fetcher: (Range) -> MastodonRequest>): List { 71 | return try { 72 | val statusPageable = fetcher.invoke(Range()).execute() 73 | val statuses = statusPageable.part 74 | Platform.runLater({ 75 | feed += statuses 76 | }) 77 | statuses 78 | } catch (e: Mastodon4jRequestException) { 79 | logger.error("Error fetching feed: " + e.localizedMessage, e) 80 | emptyList() 81 | } 82 | } 83 | 84 | fun monitorPublicFeed(feed: ObservableList, 85 | listener: (Handler) -> Shutdownable) 86 | : Shutdownable { 87 | 88 | return listener(object : Handler { 89 | override fun onStatus(status: Status) { 90 | Platform.runLater({ 91 | // logger.info { "receiving status: ${status.content}" } 92 | feed.add(element = status, index = 0) 93 | }) 94 | } 95 | 96 | override fun onNotification(notification: Notification) { 97 | } 98 | 99 | override fun onDelete(id: Long) {/* no op */ 100 | } 101 | }) 102 | } 103 | 104 | fun monitorUserFeeds(homeFeed: ObservableList, 105 | notificationFeed: ObservableList, 106 | client: MastodonClient, 107 | onNotification: (Notification) -> Unit) 108 | : Shutdownable { 109 | 110 | return buildStreamingClient(client).user(object : Handler { 111 | override fun onStatus(status: Status) { 112 | Platform.runLater({ 113 | homeFeed.add(element = status, index = 0) 114 | }) 115 | } 116 | 117 | override fun onNotification(notification: Notification) { 118 | Platform.runLater({ 119 | logger.info { "receiving notification: ${notification.type}" } 120 | notificationFeed.add(element = notification, index = 0) 121 | onNotification(notification) 122 | }) 123 | } 124 | 125 | override fun onDelete(id: Long) {/* no op */ 126 | } 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/wakingrufus/mastodon/ui/StatusFeedsView.kt: -------------------------------------------------------------------------------- 1 | package com.github.wakingrufus.mastodon.ui 2 | 3 | import com.github.wakingrufus.mastodon.client.parseUrl 4 | import com.github.wakingrufus.mastodon.data.AccountState 5 | import com.github.wakingrufus.mastodon.data.StatusFeed 6 | import com.github.wakingrufus.mastodon.toot.boostToot 7 | import com.github.wakingrufus.mastodon.toot.unboostToot 8 | import com.github.wakingrufus.mastodon.ui.styles.DefaultStyles 9 | import javafx.collections.ObservableList 10 | import javafx.geometry.Pos 11 | import javafx.scene.control.ScrollPane 12 | import javafx.scene.paint.Color 13 | import javafx.stage.StageStyle 14 | import mu.KLogging 15 | import tornadofx.* 16 | 17 | class StatusFeedsView : View() { 18 | companion object : KLogging() 19 | 20 | val statusFeeds: ObservableList by param() 21 | val accounts: ObservableList by param() 22 | val parseUrlFunc: (String) -> String by param(defaultValue = ::parseUrl) 23 | 24 | override val root = hbox { 25 | style { 26 | minWidth = 100.percent 27 | minHeight = 100.percent 28 | backgroundColor = multi(DefaultStyles.backdropColor) 29 | padding = CssBox(top = 1.px, right = 1.px, bottom = 1.px, left = 1.px) 30 | } 31 | children.bind(statusFeeds) { 32 | vbox { 33 | style { 34 | backgroundColor = multi(DefaultStyles.backdropColor) 35 | textFill = Color.WHITE 36 | padding = box(1.px, 1.px, 1.px, 1.px) 37 | alignment = Pos.CENTER 38 | maxWidth = 40.em 39 | } 40 | label(it.name + " @ " + it.server) { 41 | textFill = Color.WHITE 42 | style { 43 | backgroundColor = multi(DefaultStyles.backdropColor) 44 | padding = CssBox(1.px, 1.px, 1.px, 1.px) 45 | fontSize = 2.5.em 46 | } 47 | } 48 | scrollpane { 49 | hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER 50 | vbox { 51 | children.bind(it.statuses) { status -> 52 | vbox { 53 | addClass(DefaultStyles.defaultBorder) 54 | style { 55 | padding = CssBox(top = 1.em, bottom = 1.em, left = 1.em, right = 1.em) 56 | alignment = Pos.TOP_LEFT 57 | backgroundColor = multi(DefaultStyles.backgroundColor) 58 | textFill = Color.WHITE 59 | } 60 | this += find(mapOf( 61 | "account" to status.account!!, 62 | "server" to parseUrlFunc(status.uri))) 63 | val toot = parseToot(status.content) 64 | toot.style { 65 | backgroundColor = multi(DefaultStyles.backgroundColor) 66 | textFill = Color.WHITE 67 | } 68 | this += toot 69 | hbox { 70 | button("↰") { 71 | addClass(DefaultStyles.smallButton) 72 | action { 73 | val modal: AccountChooserView = 74 | find(mapOf("accounts" to accounts)).apply { 75 | openModal( 76 | stageStyle = StageStyle.UTILITY, 77 | block = true) 78 | } 79 | val account = modal.getAccount() 80 | logger.info { "account chosen: $account" } 81 | if (account != null) { 82 | find(mapOf( 83 | "client" to account.client, 84 | "inReplyTo" to status)).apply { 85 | openModal(stageStyle = StageStyle.UTILITY, block = true) 86 | } 87 | } 88 | 89 | } 90 | } 91 | button("☆") { 92 | if (status.isFavourited) text = "★" 93 | addClass(DefaultStyles.smallButton) 94 | } 95 | button("♲") { 96 | addClass(DefaultStyles.smallButton) 97 | if (status.isReblogged) text = "♻" 98 | action { 99 | val modal: AccountChooserView = 100 | find(mapOf("accounts" to accounts)).apply { 101 | openModal( 102 | stageStyle = StageStyle.UTILITY, 103 | block = true) 104 | } 105 | val account = modal.getAccount() 106 | logger.info { "account chosen: $account" } 107 | if (account != null) { 108 | if (status.isReblogged) { 109 | this.text = "♲" 110 | unboostToot(id = status.id, client = account.client) 111 | } else { 112 | this.text = "♻" 113 | boostToot(id = status.id, client = account.client) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } --------------------------------------------------------------------------------