diff --git a/client/app/build.gradle b/client/app/build.gradle index 2d006e2..d9f11ac 100644 --- a/client/app/build.gradle +++ b/client/app/build.gradle @@ -53,6 +53,8 @@ dependencies { // Retrofit2 for REST API implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' // 네이버 지도 SDK implementation 'com.naver.maps:map-sdk:3.12.0' // Obj - a simple Wavefront OBJ file loader @@ -67,4 +69,6 @@ dependencies { implementation("com.gorisse.thomas.sceneform:sceneform:1.19.5") // Google Location Service implementation 'com.google.android.gms:play-services-location:16.0.0' -} \ No newline at end of file + // Glide Image loading framework + implementation 'com.github.bumptech.glide:glide:4.12.0' +} diff --git a/client/app/src/main/java/com/sinbaram/mapgo/API/ServerClient.kt b/client/app/src/main/java/com/sinbaram/mapgo/API/ServerClient.kt index 0a82276..96311e4 100644 --- a/client/app/src/main/java/com/sinbaram/mapgo/API/ServerClient.kt +++ b/client/app/src/main/java/com/sinbaram/mapgo/API/ServerClient.kt @@ -3,11 +3,17 @@ package com.sinbaram.mapgo.API import com.sinbaram.mapgo.Model.CheckInResponse import com.sinbaram.mapgo.Model.CheckInRequest import com.sinbaram.mapgo.Model.Recommendation +import com.sinbaram.mapgo.Model.PostFeedItem +import com.sinbaram.mapgo.Model.Comment import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Query +import retrofit2.http.Path +import retrofit2.http.Multipart +import retrofit2.http.Part +import retrofit2.http.HTTP import retrofit2.Call /** Collections of naver openapi search interface */ @@ -34,4 +40,39 @@ interface ServerClient { @Query("epsilon") epsilon: Int, @Query("keywords") keywords: String ): Call> + + /** Get posts from mapgo server */ + @GET("Mapgo/sns/post/") + fun GetPosts() : Call> + + /** Get comments from post with {id} */ + @GET("Mapgo/sns/post/{id}/comment/") + fun GetComments( + @Path("id") id: Int + ) : Call> + + /** Post comment to post with {id} */ + @Multipart + @POST("Mapgo/sns/post/{id}/comment/") + fun PostComment( + @Path("id") id: Int, + @Part("writer") writer: Int, + @Part("contents") contents: String + ) : Call + + /** add like to post with {id}, as userID */ + @Multipart + @POST("Mapgo/sns/post/{id}/like/") + fun AddLike( + @Path("id") id: Int, + @Part("userID") userID: Int + ) : Call + + /** remove like to post with {id}, as userID */ + @Multipart + @HTTP(method="DELETE", hasBody=true, path="Mapgo/sns/post/{id}/like/") + fun DeleteLike( + @Path("id") id: Int, + @Part("userID") userID: Int + ) : Call } diff --git a/client/app/src/main/java/com/sinbaram/mapgo/API/ServerPostAPI.kt b/client/app/src/main/java/com/sinbaram/mapgo/API/ServerPostAPI.kt new file mode 100644 index 0000000..2a23880 --- /dev/null +++ b/client/app/src/main/java/com/sinbaram/mapgo/API/ServerPostAPI.kt @@ -0,0 +1,25 @@ +package com.sinbaram.mapgo.API + +import com.sinbaram.mapgo.BuildConfig +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory + +/** Retrofit singleton instance for mapgo server post + * ServerAPI's MoshiConverterFactory was not suitable as response + * of PostFeedItem and String, so changed to Gson and Scalars each. + **/ +object ServerPostAPI { + var BASE_URL: String = BuildConfig.SERVER_ADDRESS + var retrofit: Retrofit? = null + fun GetSnsClient(): Retrofit? { + if (retrofit == null) { + retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + return retrofit + } +} diff --git a/client/app/src/main/java/com/sinbaram/mapgo/CommentRecyclerAdapter.kt b/client/app/src/main/java/com/sinbaram/mapgo/CommentRecyclerAdapter.kt new file mode 100644 index 0000000..a9c7fea --- /dev/null +++ b/client/app/src/main/java/com/sinbaram/mapgo/CommentRecyclerAdapter.kt @@ -0,0 +1,56 @@ +package com.sinbaram.mapgo + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide + + +/** Recycler adapter to show comments from List of Map containing content, writer, writer image */ +class CommentRecyclerAdapter internal constructor(var context: Context, list: MutableList>?) : + RecyclerView.Adapter() { + private var mData: MutableList>? = null + + /** initialize views of comment_recycler.xml */ + inner class ViewHolder internal constructor(itemView: View) : + RecyclerView.ViewHolder(itemView) { + var content: TextView + var writer: TextView + var image: ImageView + + init { + content = itemView.findViewById(R.id.commentContent) + writer = itemView.findViewById(R.id.commentWriter) + image = itemView.findViewById(R.id.commentImage) + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val context: Context = parent.context + val inflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val view: View = inflater.inflate(R.layout.comment_recycler, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.writer.text = mData!![position]["writer"] + holder.content.text = mData!![position]["contents"] + Glide.with(context).load(mData!![position]["writerImage"]).into(holder.image) + } + + override fun getItemCount(): Int { + return mData!!.size + } + + init { + mData = list + } +} diff --git a/client/app/src/main/java/com/sinbaram/mapgo/ImageSliderAdapter.kt b/client/app/src/main/java/com/sinbaram/mapgo/ImageSliderAdapter.kt new file mode 100644 index 0000000..351e612 --- /dev/null +++ b/client/app/src/main/java/com/sinbaram/mapgo/ImageSliderAdapter.kt @@ -0,0 +1,29 @@ +package com.sinbaram.mapgo + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide + +/** ViewPager2 Adapter to show images from post */ +class ImageSliderAdapter(val context: Context, imageList: MutableList) : RecyclerView.Adapter() { + + var imagelist = imageList + + /** Initialize ImageView from image_viewpager.xml */ + inner class PagerViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder + (LayoutInflater.from(parent.context).inflate(R.layout.image_viewpager, parent, false)) { + val image = itemView.findViewById(R.id.imageView) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PagerViewHolder((parent)) + + override fun onBindViewHolder(holder: PagerViewHolder, position: Int) { + Glide.with(context).load(imagelist[position]).into(holder.image) + + } + + override fun getItemCount(): Int = imagelist.size +} diff --git a/client/app/src/main/java/com/sinbaram/mapgo/Model/PostFeedItem.kt b/client/app/src/main/java/com/sinbaram/mapgo/Model/PostFeedItem.kt new file mode 100644 index 0000000..c4aa0f4 --- /dev/null +++ b/client/app/src/main/java/com/sinbaram/mapgo/Model/PostFeedItem.kt @@ -0,0 +1,59 @@ +package com.sinbaram.mapgo.Model + +import java.io.Serializable + +/** data class representing SNS Post + * @property comment : List of dataclass Comment + * @property contents : Content of sns post + * @property like : List of dataclass Like which contains liker's userId + * @property location : Location information which sns post contains + * @property postId : id of sns post + * @property postImage : List of sns post's images url + * @property postTime : String of sns post's upload time + * @property totalLikes : Number of person who liked this post + * @property writer : Writer of sns post which contains writer's deviceId, userId, profile image as String, and username + * */ +data class PostFeedItem( + val comment: List, + val contents: String, + val like: List, + val location: Location, + val postID: Int, + val postImage: List, + val postTime: String, + val totalLikes: Int, + val writer: Writer +) : Serializable + +/** data class representing single comment object */ +data class Comment( + val writer: Writer, + val contents: String, + val post: Int +) : Serializable + +/** data class representing location information, latitude as [lat], longitude as [lng] */ +data class Location( + val lat: Double, + val lng: Double +) : Serializable + +/** data class representing writer of post */ +data class Writer( + val deviceID: String, + val id: Int, + val picture: String, + val username: String +) : Serializable + +/** data class representing single image object */ +data class PostImage( + val image: String, + val post: Int +) : Serializable + +/** data class representing liker's information */ +data class Like( + val liker: Int, + val post: Int +) : Serializable diff --git a/client/app/src/main/java/com/sinbaram/mapgo/SnsFeedActivity.kt b/client/app/src/main/java/com/sinbaram/mapgo/SnsFeedActivity.kt new file mode 100644 index 0000000..8909e2c --- /dev/null +++ b/client/app/src/main/java/com/sinbaram/mapgo/SnsFeedActivity.kt @@ -0,0 +1,248 @@ +package com.sinbaram.mapgo + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.* +import androidx.annotation.UiThread +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.MapFragment +import com.naver.maps.map.NaverMap +import com.naver.maps.map.OnMapReadyCallback +import com.naver.maps.map.overlay.Marker +import com.sinbaram.mapgo.API.ServerClient +import com.sinbaram.mapgo.API.ServerPostAPI +import com.sinbaram.mapgo.Model.Comment +import com.sinbaram.mapgo.Model.Like +import com.sinbaram.mapgo.Model.PostFeedItem +import com.sinbaram.mapgo.databinding.ActivitySnsFeedBinding +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +/** Activity to show SNS Feed + * + * Previous activity should send single sns object as name "data" + * ex. + * val data = response.body()!![1] + * nextIntent.putExtra("data", data) + * + * Also, val userId should be user's id who logged in. + * */ +class SnsFeedActivity : AppCompatActivity(), OnMapReadyCallback { + val userId : Int = 2 // this value will get from MapGoActivity + val baseurl = BuildConfig.SERVER_ADDRESS + val postImageList = mutableListOf() + val commentList = mutableListOf>() + val likerList = mutableListOf() + val serverAPI = ServerPostAPI.GetSnsClient()!!.create(ServerClient::class.java) + val marker = Marker() + private lateinit var binding : ActivitySnsFeedBinding + val TAG_SUCCESS = "success" + val TAG_FAILURE = "error" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySnsFeedBinding.inflate(layoutInflater) + setContentView(binding.root) + + // load sns post's data + val postInfo = intent.getSerializableExtra("data") as PostFeedItem + val snsfeed = SnsFeedInformation(postInfo, baseurl) + marker.position = LatLng(snsfeed.postLat, snsfeed.postLong) + for (likeobj in snsfeed.liker) { + likerList += likeobj.liker + } + snsfeed.likerCount = likerList.size + for (imageobj in postInfo.postImage) { + postImageList += baseurl + imageobj.image + } + + val fm = supportFragmentManager + val mapFragment = fm.findFragmentById(R.id.map) as MapFragment? + ?: MapFragment.newInstance().also { + fm.beginTransaction().add(R.id.map, it).commit() + } + mapFragment.getMapAsync(this) + + // set like button visiblity by the user liked/not liked status for post + val isUserLikedThisPost = likerList.contains(userId) + if (isUserLikedThisPost) { + binding.likedButton.setVisibility(View.VISIBLE) + binding.unlikedButton.setVisibility(View.INVISIBLE) + } + else { + binding.likedButton.setVisibility(View.INVISIBLE) + binding.unlikedButton.setVisibility(View.VISIBLE) + } + + // set post's username, user image, post time, content + setViewWithFeedInformation(snsfeed) + binding.imageSlider.adapter = ImageSliderAdapter(this, postImageList) + binding.commentRecycler.layoutManager = LinearLayoutManager(this) + binding.commentRecycler.setHasFixedSize(false) + getPostComment(snsfeed.postId) + + binding.commentButton.setOnClickListener(View.OnClickListener { + val comment_toPost : String = binding.commentEdittext.text.toString() + uploadPostComment(snsfeed.postId, userId, comment_toPost) + }) + binding.likeCount.setText(snsfeed.likerCount.toString()) + binding.unlikedButton.setOnClickListener(View.OnClickListener { + setPostLike(snsfeed.postId, userId) + setLikeStatus(snsfeed, false) + }) + binding.likedButton.setOnClickListener(View.OnClickListener { + removePostLike(snsfeed.postId, userId) + setLikeStatus(snsfeed, true) + }) + } + + @UiThread + override fun onMapReady(map: NaverMap) { + marker.map = map + val cameraUpdate = CameraUpdate.scrollTo(marker.position) + map.moveCamera(cameraUpdate) + } + + /** set post's username, user image, post time, content */ + fun setViewWithFeedInformation(snsfeed : SnsFeedInformation) { + binding.userName.text = snsfeed.writerName + binding.postTime.text = snsfeed.postTime + binding.content.text = snsfeed.postcontent + Glide.with(this).load(snsfeed.writerImage).into(binding.userImage) + } + + /** set button, liker count as current user's like status of current post + * @Param snsfeed : class SnsFeedInformation which contains information of sns feed + * @Param isCurrentlyLike : 'current' ike status of user. + * true if current user liked to this post(heart is red), + * false if current user unliked to this post(heart is empty) + * */ + fun setLikeStatus(snsfeed: SnsFeedInformation, isCurrentlyLike: Boolean) { + if (isCurrentlyLike) { + binding.likedButton.setVisibility(View.INVISIBLE) + binding.unlikedButton.setVisibility(View.VISIBLE) + snsfeed.likerCount-- + } + else { + binding.unlikedButton.setVisibility(View.INVISIBLE) + binding.likedButton.setVisibility(View.VISIBLE) + snsfeed.likerCount++ + } + binding.likeCount.setText(snsfeed.likerCount.toString()) + } + + /** Get comment from post + * @param postId : id of sns post + * */ + fun getPostComment(postId : Int) { + commentList.clear() + val apiCall : Call> = serverAPI.GetComments(postId) + apiCall.enqueue(object: Callback> { + override fun onResponse(call: Call>, response: Response>) { + if (response.code() == 200) { + val commentResponseList : List + commentResponseList = response.body()!! + for (commentObj in commentResponseList) { + val commentMap = mapOf("writer" to commentObj.writer.username, "writerImage" to baseurl+commentObj.writer.picture, "contents" to commentObj.contents) + commentList += commentMap + } + binding.commentRecycler.adapter = null + binding.commentRecycler.layoutManager = null + val adapter = CommentRecyclerAdapter(this@SnsFeedActivity, commentList) + binding.commentRecycler.adapter = adapter + binding.commentRecycler.layoutManager = LinearLayoutManager(this@SnsFeedActivity) + } + } + + override fun onFailure(call: Call>, t: Throwable) { + Log.d("respError", t.toString()) + } + }) + } + + /** upload comment to post + * @param postId : id of post which want to upload comments to + * @param userId : id of user which will be author of comment + * @param comment : content of comment + * */ + fun uploadPostComment(postId : Int, userId : Int, comment : String) { + val apiCall : Call = serverAPI.PostComment(postId, userId, comment) + apiCall.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + Log.d(TAG_SUCCESS, "comment done") + getPostComment(postId) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d(TAG_FAILURE, "failed to upload comment") + } + }) + } + + /** add like to post + * @param postId : id of post which added like to + * @param userId : id of user who liked to this post + */ + fun setPostLike(postId : Int, userId : Int) { + val apiCall : Call = serverAPI.AddLike(postId, userId) + apiCall.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + Log.d(TAG_SUCCESS, "add like done") + likerList+=userId + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d(TAG_FAILURE, "failed to add like") + } + }) + } + + /** remove like to post + * @param postId : id of post which removed like to + * @param userId : id of user who unliked to this post + */ + fun removePostLike(postId : Int, userId : Int) { + val apiCall : Call = serverAPI.DeleteLike(postId, userId) + apiCall.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + Log.d(TAG_SUCCESS, "remove like done") + likerList.remove(userId) + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d(TAG_FAILURE, "failed to remove like") + } + }) + } +} + +/** class of informations of sns feed */ +data class SnsFeedInformation( + val postId: Int, + val writerName: String, + val writerImage: String, + val postcontent: String, + val postTime: String, + val postLat: Double, + val postLong: Double, + val liker: List, + var likerCount: Int, +) { + constructor(postInfo: PostFeedItem, baseurl : String) : this( + postInfo.postID, + postInfo.writer.username, + baseurl + postInfo.writer.picture, + postInfo.contents, + postInfo.postTime.split(".")[0].replace("T", " "), + postInfo.location.lat, + postInfo.location.lng, + postInfo.like, + 0, + ) +} diff --git a/client/app/src/main/res/drawable/heart_filled.xml b/client/app/src/main/res/drawable/heart_filled.xml new file mode 100644 index 0000000..caa32d2 --- /dev/null +++ b/client/app/src/main/res/drawable/heart_filled.xml @@ -0,0 +1,5 @@ + + + diff --git a/client/app/src/main/res/drawable/heart_unfilled.xml b/client/app/src/main/res/drawable/heart_unfilled.xml new file mode 100644 index 0000000..c6dd9c8 --- /dev/null +++ b/client/app/src/main/res/drawable/heart_unfilled.xml @@ -0,0 +1,10 @@ + + + diff --git a/client/app/src/main/res/layout/activity_sns_feed.xml b/client/app/src/main/res/layout/activity_sns_feed.xml new file mode 100644 index 0000000..636d805 --- /dev/null +++ b/client/app/src/main/res/layout/activity_sns_feed.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +