/* Copyright 2018 Conny Duck * * This file is a part of Tusky. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation; either version 3 of the * License, or (at your option) any later version. * * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ package com.keylesspalace.tusky import android.content.Intent import android.os.Bundle import android.support.annotation.VisibleForTesting import android.support.design.widget.BottomSheetBehavior import android.view.View import android.widget.LinearLayout import com.keylesspalace.tusky.entity.SearchResults import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.LinkHelper import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.net.URI import java.net.URISyntaxException import javax.inject.Inject /** this is the base class for all activities that open links * links are checked against the api if they are mastodon links so they can be openend in Tusky * Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierachy */ abstract class BottomSheetActivity : BaseActivity() { lateinit var bottomSheet: BottomSheetBehavior var searchUrl: String? = null @Inject lateinit var mastodonApi: MastodonApi override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) val bottomSheetLayout: LinearLayout = findViewById(R.id.item_status_bottom_sheet) bottomSheet = BottomSheetBehavior.from(bottomSheetLayout) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { cancelActiveSearch() } } override fun onSlide(bottomSheet: View, slideOffset: Float) {} }) } open fun viewUrl(url: String) { if (!looksLikeMastodonUrl(url)) { openLink(url) return } val call = mastodonApi.search(url, true) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (getCancelSearchRequested(url)) { return } onEndSearch(url) if (response.isSuccessful) { // According to the mastodon API doc, if the search query is a url, // only exact matches for statuses or accounts are returned // which is good, because pleroma returns a different url // than the public post link val searchResult = response.body() if(searchResult != null) { if (searchResult.statuses.isNotEmpty()) { viewThread(searchResult.statuses[0]) return } else if (searchResult.accounts.isNotEmpty()) { viewAccount(searchResult.accounts[0].id) return } } } openLink(url) } override fun onFailure(call: Call, t: Throwable) { if (!getCancelSearchRequested(url)) { onEndSearch(url) openLink(url) } } }) callList.add(call) onBeginSearch(url) } open fun viewThread(status: Status) { if (!isSearching()) { val intent = Intent(this, ViewThreadActivity::class.java) intent.putExtra("id", status.actionableId) intent.putExtra("url", status.actionableStatus.url) startActivity(intent) } } open fun viewAccount(id: String) { val intent = Intent(this, AccountActivity::class.java) intent.putExtra("id", id) startActivity(intent) } @VisibleForTesting fun onBeginSearch(url: String) { searchUrl = url showQuerySheet() } @VisibleForTesting fun getCancelSearchRequested(url: String): Boolean { return url != searchUrl } @VisibleForTesting fun isSearching(): Boolean { return searchUrl != null } @VisibleForTesting fun onEndSearch(url: String?) { if (url == searchUrl) { // Don't clear query if there's no match, // since we might just now be getting the response for a canceled search searchUrl = null hideQuerySheet() } } @VisibleForTesting fun cancelActiveSearch() { if (isSearching()) { onEndSearch(searchUrl) } } @VisibleForTesting open fun openLink(url: String) { LinkHelper.openLink(url, this) } private fun showQuerySheet() { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED } private fun hideQuerySheet() { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN } } // https://mastodon.foo.bar/@User // https://mastodon.foo.bar/@User/43456787654678 // https://pleroma.foo.bar/users/User // https://pleroma.foo.bar/users/43456787654678 // https://pleroma.foo.bar/notice/43456787654678 // https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207 fun looksLikeMastodonUrl(urlString: String): Boolean { val uri: URI try { uri = URI(urlString) } catch (e: URISyntaxException) { return false } if (uri.query != null || uri.fragment != null || uri.path == null) { return false } val path = uri.path return path.matches("^/@[^/]+$".toRegex()) || path.matches("^/users/[^/]+$".toRegex()) || path.matches("^/@[^/]+/\\d+$".toRegex()) || path.matches("^/notice/\\d+$".toRegex()) || path.matches("^/objects/[-a-f0-9]+$".toRegex()) }