Added wikitalk page and improved bottomsheet for landscape mode

This commit is contained in:
Kanahia 2024-05-23 20:07:07 +05:30
parent 4545059035
commit 5f8592eb0a
18 changed files with 713 additions and 225 deletions

View file

@ -14,6 +14,7 @@ import fr.free.nrw.commons.description.DescriptionEditActivity;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.media.ZoomableActivity; import fr.free.nrw.commons.media.ZoomableActivity;
import fr.free.nrw.commons.nearby.WikidataFeedback;
import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.profile.ProfileActivity; import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity; import fr.free.nrw.commons.review.ReviewActivity;
@ -79,4 +80,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract ZoomableActivity bindZoomableActivity(); abstract ZoomableActivity bindZoomableActivity();
@ContributesAndroidInjector
abstract WikidataFeedback bindWikiFeedback();
} }

View file

@ -2,9 +2,11 @@ package fr.free.nrw.commons.di;
import com.google.gson.Gson; import com.google.gson.Gson;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.explore.categories.CategoriesModule; import fr.free.nrw.commons.explore.categories.CategoriesModule;
import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetFragment;
import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment;
import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.upload.worker.UploadWorker; import fr.free.nrw.commons.upload.worker.UploadWorker;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -68,6 +70,9 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PicOfDayAppWidget picOfDayAppWidget); void inject(PicOfDayAppWidget picOfDayAppWidget);
@Singleton
void inject(NearbyController nearbyController);
Gson gson(); Gson gson();
@Component.Builder @Component.Builder

View file

@ -11,10 +11,10 @@ import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.explore.depictions.DepictsClient; import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.PlaceBindings;
import fr.free.nrw.commons.nearby.model.ItemsClass; import fr.free.nrw.commons.nearby.model.ItemsClass;
import fr.free.nrw.commons.nearby.model.NearbyResponse; import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem; import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.nearby.model.PlaceBindings;
import fr.free.nrw.commons.profile.achievements.FeaturedImages; import fr.free.nrw.commons.profile.achievements.FeaturedImages;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse; import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
@ -397,6 +397,31 @@ public class OkHttpJsonApiClient {
throw new Exception(response.message()); throw new Exception(response.message());
} }
@Nullable
public String getWikidataTalk(String wikidataTitle)
throws Exception {
Timber.tag("PRINT").e(wikidataTitle);
final HttpUrl.Builder urlBuilder = HttpUrl
.parse("https://www.wikidata.org/w/api.php").newBuilder()
.addQueryParameter("action", "query")
.addQueryParameter("prop", "revisions")
.addQueryParameter("rvprop", "content")
.addQueryParameter("rvslots", "main")
.addQueryParameter("titles", wikidataTitle)
.addQueryParameter("format", "json");
final Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
final Response response = okHttpClient.newCall(request).execute();
if (response.body() != null && response.isSuccessful()) {
final String json = response.body().string();
return json;
}
throw new Exception(response.message());
}
/** /**
* Make API Call to get Places * Make API Call to get Places
* *
@ -476,7 +501,7 @@ public class OkHttpJsonApiClient {
" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">" " xsi:schemaLocation=\"http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd\">"
+ "\n<bounds minlat=\"$MIN_LATITUDE\" minlon=\"$MIN_LONGITUDE\" maxlat=\"$MAX_LATITUDE\" maxlon=\"$MAX_LONGITUDE\"/>"; + "\n<bounds minlat=\"$MIN_LATITUDE\" minlon=\"$MIN_LONGITUDE\" maxlat=\"$MAX_LATITUDE\" maxlon=\"$MAX_LONGITUDE\"/>";
List<PlaceBindings> placeBindings = runQuery(leftLatLng,rightLatLng); List<PlaceBindings> placeBindings = runQuery(leftLatLng, rightLatLng);
if (placeBindings != null) { if (placeBindings != null) {
for (PlaceBindings item : placeBindings) { for (PlaceBindings item : placeBindings) {
if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) { if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) {

View file

@ -0,0 +1,92 @@
package fr.free.nrw.commons.nearby
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnLongClickListener
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.NonNull
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.nearby.model.BottomSheetItem
class BottomSheetAdapter(context: Context?, private val itemList: List<BottomSheetItem>) :
RecyclerView.Adapter<BottomSheetAdapter.ViewHolder>() {
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
private var itemClickListener: ItemClickListener? = null
@NonNull
override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder {
val view: View = layoutInflater.inflate(R.layout.bottom_sheet_item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(@NonNull holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.imageView.setImageDrawable(
ContextCompat.getDrawable(
getContext(),
item.imageResourceId
)
)
holder.title.setText(item.title)
}
override fun getItemCount(): Int {
return itemList.size
}
fun updateBookmarkIcon(icon: Int) {
itemList.forEachIndexed { index, item ->
if (item.imageResourceId == R.drawable.ic_round_star_filled_24px || item.imageResourceId == R.drawable.ic_round_star_border_24px) {
item.imageResourceId = icon
this.notifyItemChanged(index)
return
}
}
}
inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, OnLongClickListener {
var imageView: ImageView = itemView.findViewById(R.id.buttonImage)
var title: TextView = itemView.findViewById(R.id.buttonText)
init {
itemView.setOnClickListener(this)
itemView.setOnLongClickListener(this)
}
override fun onClick(view: View) {
if (itemClickListener != null) itemClickListener!!.onBottomSheetItemClick(
view,
adapterPosition
)
}
override fun onLongClick(view: View): Boolean {
if (itemClickListener != null) itemClickListener!!.onBottomSheetItemLongClick(
view,
adapterPosition
)
return true
}
}
fun setClickListener(itemClickListener: ItemClickListener?) {
this.itemClickListener = itemClickListener
}
fun getContext(): Context {
return layoutInflater.context
}
interface ItemClickListener {
fun onBottomSheetItemClick(view: View?, position: Int)
fun onBottomSheetItemLongClick(view: View?, position: Int)
}
}

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.nearby
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.nearby.model.Description
class DescriptionAdapter(private val descriptions: List<Description>) :
RecyclerView.Adapter<DescriptionAdapter.DescriptionViewHolder>() {
class DescriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val descTextView: TextView = itemView.findViewById(R.id.descTextView)
val userTextView: TextView = itemView.findViewById(R.id.userTextView)
val dateTextView: TextView = itemView.findViewById(R.id.dateTextView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DescriptionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_wikitalk_item_description, parent, false)
return DescriptionViewHolder(view)
}
override fun onBindViewHolder(holder: DescriptionViewHolder, position: Int) {
val description = descriptions[position]
holder.descTextView.text = description.text
holder.userTextView.text = description.user
holder.dateTextView.text = description.time
}
override fun getItemCount(): Int {
return descriptions.size
}
}

View file

@ -131,6 +131,10 @@ public class NearbyController extends MapController {
); );
} }
public String getWikiTalk(String s) throws Exception {
return nearbyPlaces.getWikiTalk(s);
}
public static LatLng calculateNorthEast(double latitude, double longitude, double distance) { public static LatLng calculateNorthEast(double latitude, double longitude, double distance) {
double lat1 = Math.toRadians(latitude); double lat1 = Math.toRadians(latitude);
double deltaLat = distance * 0.008; double deltaLat = distance * 0.008;

View file

@ -120,6 +120,10 @@ public class NearbyPlaces {
customQuery); customQuery);
} }
public String getWikiTalk(String QID) throws Exception {
return okHttpJsonApiClient.getWikidataTalk(QID);
}
/** /**
* Runs the Wikidata query to retrieve the KML String * Runs the Wikidata query to retrieve the KML String
* *

View file

@ -0,0 +1,38 @@
package fr.free.nrw.commons.nearby
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.nearby.model.Title
class TitleAdapter(private val titles: List<Title>) :
RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
class TitleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
val descriptionsRecyclerView: RecyclerView = itemView.findViewById(R.id.descriptionsRecyclerView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_wikitalk_item, parent, false)
return TitleViewHolder(view)
}
override fun onBindViewHolder(holder: TitleViewHolder, position: Int) {
val title = titles[position]
holder.titleTextView.text = title.title
holder.descriptionsRecyclerView.apply {
layoutManager = LinearLayoutManager(holder.itemView.context)
adapter = DescriptionAdapter(title.descriptions)
}
}
override fun getItemCount(): Int {
return titles.size
}
}

View file

@ -0,0 +1,142 @@
package fr.free.nrw.commons.nearby
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.databinding.ActivityWikidataFeedbackBinding
import fr.free.nrw.commons.nearby.model.Description
import fr.free.nrw.commons.nearby.model.Title
import fr.free.nrw.commons.theme.BaseActivity
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import timber.log.Timber
import javax.inject.Inject
class WikidataFeedback : BaseActivity() {
private lateinit var binding: ActivityWikidataFeedbackBinding
var place: String = ""
var wikidataQId: String = ""
@Inject
lateinit var nearbyController: NearbyController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWikidataFeedbackBinding.inflate(layoutInflater)
setContentView(binding.root)
place = intent.getStringExtra("place") ?: ""
wikidataQId = intent.getStringExtra("qid") ?: ""
binding.toolbarBinding.toolbar.title = "Talk:" + wikidataQId
binding.textHeader.text = "Write something about the "+"'$place'"+" item. It will be publicly visible."
setSupportActionBar(binding.toolbarBinding.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
getWikidataFeedback(place, wikidataQId)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
/**
* This function starts the Wikidata feedback activity of the selected place
* The API returns feedback given by other users
*/
@SuppressLint("CheckResult")
fun getWikidataFeedback(name: String, wikidataQID: String?) {
try {
val wikiTalkObservable = Observable
.fromCallable {
nearbyController.getWikiTalk("Talk:"+wikidataQID)
}
compositeDisposable.add(
wikiTalkObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
if (result != null) {
Timber.tag("PRINT").d("$result")
val key = "\"*\":\""
var startIndex = result.indexOf(key)
startIndex += key.length
var endIndex = result.indexOf("\"", startIndex)
while (endIndex != -1 && result[endIndex - 1] == '\\') {
endIndex = result.indexOf("\"", endIndex + 1)
}
var value = result.substring(startIndex, endIndex)
value = value.replace("\\n", "\n").replace("\\\"", "\"")
Timber.tag("PRINT").e(value);
updateUi(name, value, extractData(value))
} else {
Timber.d("result is null")
Toast.makeText(this, "Failed", Toast.LENGTH_SHORT).show()
}
}, { throwable ->
Timber.e(throwable, "Error occurred while loading notifications")
throwable.printStackTrace()
})
)
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun updateUi(place: String, feedback: String,titles: List<Title> ) {
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = TitleAdapter(titles)
binding.progressBar.visibility = View.GONE
binding.activityLayout.visibility = View.VISIBLE
binding.descText.text = if (feedback.isNotEmpty()) feedback else "No Feedback"
}
fun extractData(input: String): List<Title> {
val titlePattern = Regex("""==\s*(.*?)\s*==""")
val descriptionPattern = Regex("==\\n(.*?\\.)")
val userPattern = Regex("""\[\[User:(.*?)\|""")
val timestampPattern = Regex("""\d{2}:\d{2}, \d+ \w+ \d{4} \(UTC\)""")
val titles = titlePattern.findAll(input).map { it.groupValues[1] }.toList()
val descriptions = descriptionPattern.findAll(input).map { it.groupValues[1] }.toList()
val users = userPattern.findAll(input).map { it.groupValues[1] }.toList()
val timestamps = timestampPattern.findAll(input).map { it.value }.toList()
val groupedDescriptions = mutableListOf<Description>()
for (i in 0 until minOf(descriptions.size, users.size, timestamps.size)) {
groupedDescriptions.add(Description(descriptions[i], users[i], timestamps[i]))
}
val titleToDescriptions = mutableMapOf<String, MutableList<Description>>()
var currentTitle: String? = null
input.lines().forEach { line ->
val titleMatch = titlePattern.matchEntire(line)
if (titleMatch != null) {
currentTitle = titleMatch.groupValues.getOrNull(1)
currentTitle?.let {
titleToDescriptions[it] = mutableListOf()
}
} else if (!currentTitle.isNullOrBlank()) {
groupedDescriptions.removeFirstOrNull()
?.let { titleToDescriptions[currentTitle]?.add(it) }
}
}
return titleToDescriptions.map { (title, descriptions) -> Title(title, descriptions) }
}
}

View file

@ -48,6 +48,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@ -68,9 +69,9 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationPermissionsHelper; import fr.free.nrw.commons.location.LocationPermissionsHelper;
import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.nearby.BottomSheetAdapter;
import fr.free.nrw.commons.nearby.CheckBoxTriStates; import fr.free.nrw.commons.nearby.CheckBoxTriStates;
import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.nearby.MarkerPlaceGroup; import fr.free.nrw.commons.nearby.MarkerPlaceGroup;
@ -78,8 +79,10 @@ import fr.free.nrw.commons.nearby.NearbyController;
import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter; import fr.free.nrw.commons.nearby.NearbyFilterSearchRecyclerViewAdapter;
import fr.free.nrw.commons.nearby.NearbyFilterState; import fr.free.nrw.commons.nearby.NearbyFilterState;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.WikidataFeedback;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback; import fr.free.nrw.commons.nearby.fragments.AdvanceQueryFragment.Callback;
import fr.free.nrw.commons.nearby.model.BottomSheetItem;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter; import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import fr.free.nrw.commons.upload.FileUtils; import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
@ -132,7 +135,7 @@ import timber.log.Timber;
public class NearbyParentFragment extends CommonsDaggerSupportFragment public class NearbyParentFragment extends CommonsDaggerSupportFragment
implements NearbyParentFragmentContract.View, implements NearbyParentFragmentContract.View,
WikidataEditListener.WikidataP18EditListener, LocationUpdateListener, WikidataEditListener.WikidataP18EditListener, LocationUpdateListener,
LocationPermissionCallback { LocationPermissionCallback, BottomSheetAdapter.ItemClickListener {
FragmentNearbyParentBinding binding; FragmentNearbyParentBinding binding;
@ -190,6 +193,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback; private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback;
private boolean isAdvancedQueryFragmentVisible = false; private boolean isAdvancedQueryFragmentVisible = false;
private Place nearestPlace; private Place nearestPlace;
private GridLayoutManager gridLayoutManager;
private List<BottomSheetItem> dataList;
private BottomSheetAdapter bottomSheetAdapter;
private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult( private ActivityResultLauncher<String[]> inAppCameraLocationPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultContracts.RequestMultiplePermissions(),
new ActivityResultCallback<Map<String, Boolean>>() { new ActivityResultCallback<Map<String, Boolean>>() {
@ -650,7 +656,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
initBottomSheets(); initBottomSheets();
loadAnimations(); loadAnimations();
setBottomSheetCallbacks(); setBottomSheetCallbacks();
decideButtonVisibilities();
addActionToTitle(); addActionToTitle();
if (!Utils.isMonumentsEnabled(new Date())) { if (!Utils.isMonumentsEnabled(new Date())) {
NearbyFilterState.setWlmSelected(false); NearbyFilterState.setWlmSelected(false);
@ -674,6 +679,15 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
} }
private int getSpanCount() {
int orientation = getResources().getConfiguration().orientation;
if (bottomSheetAdapter != null){
return (orientation == Configuration.ORIENTATION_PORTRAIT) ? 3 : bottomSheetAdapter.getItemCount();
}else {
return (orientation == Configuration.ORIENTATION_PORTRAIT) ? 3 : 6;
}
}
public void initNearbyFilter() { public void initNearbyFilter() {
binding.nearbyFilterList.getRoot().setVisibility(View.GONE); binding.nearbyFilterList.getRoot().setVisibility(View.GONE);
hideBottomSheet(); hideBottomSheet();
@ -898,20 +912,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward); rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
} }
/**
* Fits buttons according to our layout
*/
private void decideButtonVisibilities() {
// Remove button text if they exceed 1 line or if internal layout has not been built
// Only need to check for directions button because it is the longest
if ( binding.bottomSheetDetails.directionsButtonText.getLineCount() > 1 || binding.bottomSheetDetails.directionsButtonText.getLineCount() == 0) {
binding.bottomSheetDetails.wikipediaButtonText.setVisibility(View.GONE);
binding.bottomSheetDetails.wikidataButtonText.setVisibility(View.GONE);
binding.bottomSheetDetails.commonsButtonText.setVisibility(View.GONE);
binding.bottomSheetDetails.directionsButtonText.setVisibility(View.GONE);
}
}
/** /**
* *
*/ */
@ -2006,50 +2006,33 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
*/ */
private void passInfoToSheet(final Place place) { private void passInfoToSheet(final Place place) {
selectedPlace = place; selectedPlace = place;
dataList = new ArrayList<>();
// TODO: Decide button text for fitting in the screen
dataList.add(new BottomSheetItem(R.drawable.ic_round_star_border_24px, ""));
dataList.add(new BottomSheetItem(R.drawable.ic_directions_black_24dp,
getResources().getString(R.string.nearby_directions)));
if (place.hasWikidataLink()) {
dataList.add(new BottomSheetItem(R.drawable.ic_wikidata_logo_24dp,
getResources().getString(R.string.nearby_wikidata)));
}
dataList.add(new BottomSheetItem(R.drawable.ic_feedback_black_24dp,
getResources().getString(R.string.nearby_wikitalk)));
if (place.hasWikipediaLink()) {
dataList.add(new BottomSheetItem(R.drawable.ic_wikipedia_logo_24dp,
getResources().getString(R.string.nearby_wikipedia)));
}
if (selectedPlace.hasCommonsLink()) {
dataList.add(new BottomSheetItem(R.drawable.ic_commons_icon_vector,
getResources().getString(R.string.nearby_commons)));
}
int spanCount = getSpanCount();
gridLayoutManager = new GridLayoutManager(this.getContext(), spanCount);
binding.bottomSheetDetails.bottomSheetRecyclerView.setLayoutManager(gridLayoutManager);
bottomSheetAdapter = new BottomSheetAdapter(this.getContext(), dataList);
bottomSheetAdapter.setClickListener(this);
binding.bottomSheetDetails.bottomSheetRecyclerView.setAdapter(bottomSheetAdapter);
updateBookmarkButtonImage(selectedPlace); updateBookmarkButtonImage(selectedPlace);
binding.bottomSheetDetails.bookmarkButton.setOnClickListener(view -> {
final boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
updateBookmarkButtonImage(selectedPlace);
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
binding.map.invalidate();
});
binding.bottomSheetDetails.bookmarkButton.setOnLongClickListener(view -> {
Toast.makeText(getContext(), R.string.menu_bookmark, Toast.LENGTH_SHORT).show();
return true;
});
binding.bottomSheetDetails.wikipediaButton.setVisibility(place.hasWikipediaLink() ? View.VISIBLE : View.GONE);
binding.bottomSheetDetails.wikipediaButton.setOnClickListener(
view -> Utils.handleWebUrl(getContext(), selectedPlace.siteLinks.getWikipediaLink()));
binding.bottomSheetDetails.wikipediaButton.setOnLongClickListener(view -> {
Toast.makeText(getContext(), R.string.nearby_wikipedia, Toast.LENGTH_SHORT).show();
return true;
});
binding.bottomSheetDetails.wikidataButton.setVisibility(place.hasWikidataLink() ? View.VISIBLE : View.GONE);
binding.bottomSheetDetails.wikidataButton.setOnClickListener(
view -> Utils.handleWebUrl(getContext(), selectedPlace.siteLinks.getWikidataLink()));
binding.bottomSheetDetails.wikidataButton.setOnLongClickListener(view -> {
Toast.makeText(getContext(), R.string.nearby_wikidata, Toast.LENGTH_SHORT).show();
return true;
});
binding.bottomSheetDetails.directionsButton.setOnClickListener(view -> Utils.handleGeoCoordinates(getActivity(),
selectedPlace.getLocation()));
binding.bottomSheetDetails.directionsButton.setOnLongClickListener(view -> {
Toast.makeText(getContext(), R.string.nearby_directions, Toast.LENGTH_SHORT).show();
return true;
});
binding.bottomSheetDetails.commonsButton.setVisibility(selectedPlace.hasCommonsLink() ? View.VISIBLE : View.GONE);
binding.bottomSheetDetails.commonsButton.setOnClickListener(
view -> Utils.handleWebUrl(getContext(), selectedPlace.siteLinks.getCommonsLink()));
binding.bottomSheetDetails.commonsButton.setOnLongClickListener(view -> {
Toast.makeText(getContext(), R.string.nearby_commons, Toast.LENGTH_SHORT).show();
return true;
});
binding.bottomSheetDetails.icon.setImageResource(selectedPlace.getLabel().getIcon()); binding.bottomSheetDetails.icon.setImageResource(selectedPlace.getLabel().getIcon());
binding.bottomSheetDetails.title.setText(selectedPlace.name); binding.bottomSheetDetails.title.setText(selectedPlace.name);
@ -2101,9 +2084,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
} else { } else {
bookmarkIcon = R.drawable.ic_round_star_border_24px; bookmarkIcon = R.drawable.ic_round_star_border_24px;
} }
if ( binding.bottomSheetDetails.bookmarkButtonImage != null) { bottomSheetAdapter.updateBookmarkIcon(bookmarkIcon);
binding.bottomSheetDetails.bookmarkButtonImage.setImageResource(bookmarkIcon);
}
} }
@Override @Override
@ -2281,6 +2262,77 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
binding.map.getController().animateTo(geoPoint); binding.map.getController().animateTo(geoPoint);
} }
@Override
public void onBottomSheetItemClick(@Nullable View view, int position) {
BottomSheetItem item = dataList.get(position);
final boolean isBookmarked = bookmarkLocationDao.updateBookmarkLocation(selectedPlace);
switch (item.getImageResourceId()) {
case R.drawable.ic_round_star_border_24px:
updateBookmarkButtonImage(selectedPlace);
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
binding.map.invalidate();
break;
case R.drawable.ic_round_star_filled_24px:
updateBookmarkButtonImage(selectedPlace);
updateMarker(isBookmarked, selectedPlace, locationManager.getLastLocation());
binding.map.invalidate();
break;
case R.drawable.ic_directions_black_24dp:
Utils.handleGeoCoordinates(this.getContext(), selectedPlace.getLocation());
break;
case R.drawable.ic_wikidata_logo_24dp:
Utils.handleWebUrl(this.getContext(), selectedPlace.siteLinks.getWikidataLink());
break;
case R.drawable.ic_feedback_black_24dp:
Intent intent = new Intent(this.getContext(), WikidataFeedback.class);
intent.putExtra("place", selectedPlace.name);
intent.putExtra("qid", selectedPlace.getWikiDataEntityId());
startActivity(intent);
break;
case R.drawable.ic_wikipedia_logo_24dp:
Utils.handleWebUrl(this.getContext(), selectedPlace.siteLinks.getWikipediaLink());
break;
case R.drawable.ic_commons_icon_vector:
Utils.handleWebUrl(this.getContext(), selectedPlace.siteLinks.getCommonsLink());
break;
default:
break;
}
}
@Override
public void onBottomSheetItemLongClick(@Nullable View view, int position) {
BottomSheetItem item = dataList.get(position);
String message;
switch (item.getImageResourceId()) {
case R.drawable.ic_round_star_border_24px:
message = getString(R.string.menu_bookmark);
break;
case R.drawable.ic_round_star_filled_24px:
message = getString(R.string.menu_bookmark);
break;
case R.drawable.ic_directions_black_24dp:
message = getString(R.string.nearby_directions);
break;
case R.drawable.ic_wikidata_logo_24dp:
message = getString(R.string.nearby_wikidata);
break;
case R.drawable.ic_feedback_black_24dp:
message = getString(R.string.nearby_wikitalk);
break;
case R.drawable.ic_wikipedia_logo_24dp:
message = getString(R.string.nearby_wikipedia);
break;
case R.drawable.ic_commons_icon_vector:
message = getString(R.string.nearby_commons);
break;
default:
message = "Long click";
break;
}
Toast.makeText(this.getContext(), message, Toast.LENGTH_SHORT).show();
}
public interface NearbyParentFragmentInstanceReadyCallback { public interface NearbyParentFragmentInstanceReadyCallback {
void onReady(); void onReady();
@ -2298,9 +2350,12 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
rlBottomSheetLayoutParams.height = rlBottomSheetLayoutParams.height =
getActivity().getWindowManager().getDefaultDisplay().getHeight() / 16 * 9; getActivity().getWindowManager().getDefaultDisplay().getHeight() / 16 * 9;
binding.bottomSheetNearby.bottomSheet.setLayoutParams(rlBottomSheetLayoutParams); binding.bottomSheetNearby.bottomSheet.setLayoutParams(rlBottomSheetLayoutParams);
int spanCount = getSpanCount();
if (gridLayoutManager != null) {
gridLayoutManager.setSpanCount(spanCount);
}
} }
public void onLearnMoreClicked() { public void onLearnMoreClicked() {
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(WLM_URL)); intent.setData(Uri.parse(WLM_URL));

View file

@ -0,0 +1,3 @@
package fr.free.nrw.commons.nearby.model
class BottomSheetItem(var imageResourceId: Int, val title: String)

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.nearby.model
data class Description(
val text: String,
val user: String,
val time: String
)
data class Title(
val title: String,
val descriptions: List<Description>
)

View file

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".nearby.WikidataFeedback">
<LinearLayout
android:id="@+id/toolbarLayout"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/toolbarBinding"
layout="@layout/toolbar" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbarLayout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toTopOf="@+id/textHeader"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentTextLabel" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Write something about the 'GS1 Japan' item. It will be publicly visible."
android:textSize="@dimen/normal_text"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/descriptionEditText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/subjectEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:hint="Subject"
android:singleLine="false"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textHeader" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/descriptionEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:hint="Description"
android:singleLine="false"
android:visibility="visible"
app:layout_constraintBottom_toTopOf="@+id/appCompatButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/descText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:inputType="textMultiLine"
android:text="Discussion"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/currentTextLabel" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/appCompatButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="SEND"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/textHeader"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/textHeader" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/currentTextLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Current talk page:"
android:textSize="@dimen/heading_text_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -53,169 +53,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tiny_height" android:layout_height="@dimen/tiny_height"
android:layout_marginTop="@dimen/small_height" android:layout_marginTop="@dimen/small_height"
android:layout_marginBottom="@dimen/activity_margin_horizontal" android:layout_marginBottom="@dimen/activity_margin_horizontal"
android:background="@android:color/darker_gray"/> android:background="@android:color/darker_gray" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<LinearLayout <androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookmarkButton" android:id="@+id/bottom_sheet_recycler_view"
android:layout_width="@dimen/dimen_0" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:orientation="vertical"
android:background="@drawable/button_background_selector"
>
<ImageView
android:id="@+id/bookmarkButtonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:srcCompat="@drawable/ic_round_star_border_24px"
android:tint="?attr/rowButtonColor"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bookmarkButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:layout_gravity="center_horizontal"
android:text="CAMERA"
android:visibility="gone"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/directionsButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_directions_black_24dp"
android:tint="?attr/rowButtonColor"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/directionsButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_directions"
android:textAllCaps="true"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/wikidataButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_wikidata_logo_24dp"
android:tint="?attr/rowButtonColor"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/wikidataButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_wikidata"
android:textAllCaps="true"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/wikipediaButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_wikipedia_logo_24dp"
android:tint="?attr/rowButtonColor"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/wikipediaButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_wikipedia"
android:textAllCaps="true"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/commonsButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:background="@drawable/button_background_selector"
android:orientation="vertical"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:duplicateParentState="true"
app:srcCompat="@drawable/ic_commons_icon_vector"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/commonsButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:duplicateParentState="true"
android:textColor="@color/text_color_selector"
android:layout_gravity="center_horizontal"
android:text="@string/nearby_commons"
android:textAllCaps="true"
/>
</LinearLayout>
</LinearLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/tiny_height" android:layout_height="@dimen/tiny_height"
android:layout_marginTop="@dimen/small_height" android:layout_marginTop="@dimen/small_height"
android:layout_marginBottom="@dimen/activity_margin_horizontal" android:layout_marginBottom="@dimen/activity_margin_horizontal"
android:background="@android:color/darker_gray"/> android:background="@android:color/darker_gray" />
<TextView <TextView
android:id="@+id/description" android:id="@+id/description"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bookmarkButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:background="@drawable/button_background_selector"
android:clickable="true"
android:orientation="vertical"
android:padding="@dimen/standard_gap">
<ImageView
android:id="@+id/buttonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:tint="?attr/rowButtonColor" />
<TextView
android:id="@+id/buttonText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:paddingTop="@dimen/activity_margin_horizontal"
android:text="CAMERA"
android:visibility="gone" />
</LinearLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/titleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/dimen_10"
android:paddingBottom="8dp"
android:textSize="20sp"
android:text="Title"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/descriptionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:orientation="vertical">
<TextView
android:id="@+id/descTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/userTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />
<TextView
android:id="@+id/dateTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />
</LinearLayout>
</LinearLayout>

View file

@ -818,4 +818,5 @@ Upload your first media by tapping on the add button.</string>
</plurals> </plurals>
<string name="multiple_files_depiction">Please remember that all images in a multi-upload get the same categories and depictions. If the images do not share depictions and categories, please perform several separate uploads.</string> <string name="multiple_files_depiction">Please remember that all images in a multi-upload get the same categories and depictions. If the images do not share depictions and categories, please perform several separate uploads.</string>
<string name="multiple_files_depiction_header">Note about multi-uploads</string> <string name="multiple_files_depiction_header">Note about multi-uploads</string>
<string name="nearby_wikitalk">wikitalk</string>
</resources> </resources>